Skip to content

Commit c7bdbc2

Browse files
feat(config): add api config for branching override (#2761)
* feat: add remote utils for api config * chore: split api and remote api * Revert "chore: split api and remote api" This reverts commit 23173ec. * chore: make api public * Revert "Revert "chore: split api and remote api"" This reverts commit 65bc6d3. * chore: handle api enable * chore: make convert whitespace resilient * feat: add some errors handling for remotes config * chore: move diff into own package * chore: add some diff tests * chore: fix golint casting lints * Update internal/utils/cast/cast.go Co-authored-by: Han Qiao <[email protected]> * chore: use Errorf remote config error * chore: move diff and cast to pkg * chore: minor refactor * feat: implement remote config updater * chore: minor style changes * chore: refactor duplicate project ref check to getter * chore: update error message for consistency * chore: validate duplicate remote early --------- Co-authored-by: Han Qiao <[email protected]> Co-authored-by: Qiao Han <[email protected]>
1 parent fe0097e commit c7bdbc2

File tree

9 files changed

+442
-38
lines changed

9 files changed

+442
-38
lines changed

internal/link/link.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package link
22

33
import (
4-
"bytes"
54
"context"
65
"fmt"
76
"os"
87
"strconv"
98
"strings"
109
"sync"
1110

12-
"github.com/BurntSushi/toml"
1311
"github.com/go-errors/errors"
1412
"github.com/jackc/pgconn"
1513
"github.com/jackc/pgx/v4"
@@ -20,15 +18,20 @@ import (
2018
"github.com/supabase/cli/internal/utils/flags"
2119
"github.com/supabase/cli/internal/utils/tenant"
2220
"github.com/supabase/cli/pkg/api"
21+
"github.com/supabase/cli/pkg/cast"
2322
cliConfig "github.com/supabase/cli/pkg/config"
23+
"github.com/supabase/cli/pkg/diff"
2424
"github.com/supabase/cli/pkg/migration"
2525
)
2626

2727
func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
28-
original := toTomlBytes(map[string]interface{}{
28+
original, err := cliConfig.ToTomlBytes(map[string]interface{}{
2929
"api": utils.Config.Api,
3030
"db": utils.Config.Db,
3131
})
32+
if err != nil {
33+
fmt.Fprintln(utils.GetDebugLogger(), err)
34+
}
3235

3336
if err := checkRemoteProjectStatus(ctx, projectRef); err != nil {
3437
return err
@@ -60,28 +63,21 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(
6063
fmt.Fprintln(os.Stdout, "Finished "+utils.Aqua("supabase link")+".")
6164

6265
// 4. Suggest config update
63-
updated := toTomlBytes(map[string]interface{}{
66+
updated, err := cliConfig.ToTomlBytes(map[string]interface{}{
6467
"api": utils.Config.Api,
6568
"db": utils.Config.Db,
6669
})
67-
// if lineDiff := cmp.Diff(original, updated); len(lineDiff) > 0 {
68-
if lineDiff := Diff(utils.ConfigPath, original, projectRef, updated); len(lineDiff) > 0 {
70+
if err != nil {
71+
fmt.Fprintln(utils.GetDebugLogger(), err)
72+
}
73+
74+
if lineDiff := diff.Diff(utils.ConfigPath, original, projectRef, updated); len(lineDiff) > 0 {
6975
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local config differs from linked project. Try updating", utils.Bold(utils.ConfigPath))
7076
fmt.Println(string(lineDiff))
7177
}
7278
return nil
7379
}
7480

75-
func toTomlBytes(config any) []byte {
76-
var buf bytes.Buffer
77-
enc := toml.NewEncoder(&buf)
78-
enc.Indent = ""
79-
if err := enc.Encode(config); err != nil {
80-
fmt.Fprintln(utils.GetDebugLogger(), "failed to marshal toml config:", err)
81-
}
82-
return buf.Bytes()
83-
}
84-
8581
func LinkServices(ctx context.Context, projectRef, anonKey string, fsys afero.Fs) {
8682
// Ignore non-fatal errors linking services
8783
var wg sync.WaitGroup
@@ -147,7 +143,7 @@ func linkPostgrestVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.
147143
}
148144

149145
func updateApiConfig(config api.PostgrestConfigWithJWTSecretResponse) {
150-
utils.Config.Api.MaxRows = uint(config.MaxRows)
146+
utils.Config.Api.MaxRows = cast.IntToUint(config.MaxRows)
151147
utils.Config.Api.ExtraSearchPath = readCsv(config.DbExtraSearchPath)
152148
utils.Config.Api.Schemas = readCsv(config.DbSchema)
153149
}

pkg/cast/cast.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package cast
2+
3+
import "math"
4+
5+
// UintToInt converts a uint to an int, handling potential overflow
6+
func UintToInt(value uint) int {
7+
if value <= math.MaxInt {
8+
result := int(value)
9+
return result
10+
}
11+
maxInt := math.MaxInt
12+
return maxInt
13+
}
14+
15+
// IntToUint converts an int to a uint, handling negative values
16+
func IntToUint(value int) uint {
17+
if value < 0 {
18+
return 0
19+
}
20+
return uint(value)
21+
}
22+
23+
func Ptr[T any](v T) *T {
24+
return &v
25+
}

pkg/config/api.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package config
2+
3+
import (
4+
"strings"
5+
6+
v1API "github.com/supabase/cli/pkg/api"
7+
"github.com/supabase/cli/pkg/cast"
8+
"github.com/supabase/cli/pkg/diff"
9+
)
10+
11+
type (
12+
api struct {
13+
Enabled bool `toml:"enabled"`
14+
Schemas []string `toml:"schemas"`
15+
ExtraSearchPath []string `toml:"extra_search_path"`
16+
MaxRows uint `toml:"max_rows"`
17+
// Local only config
18+
Image string `toml:"-"`
19+
KongImage string `toml:"-"`
20+
Port uint16 `toml:"port"`
21+
Tls tlsKong `toml:"tls"`
22+
// TODO: replace [auth|studio].api_url
23+
ExternalUrl string `toml:"external_url"`
24+
}
25+
26+
tlsKong struct {
27+
Enabled bool `toml:"enabled"`
28+
}
29+
)
30+
31+
func (a *api) ToUpdatePostgrestConfigBody() v1API.UpdatePostgrestConfigBody {
32+
body := v1API.UpdatePostgrestConfigBody{}
33+
34+
// When the api is disabled, remote side it just set the dbSchema to an empty value
35+
if !a.Enabled {
36+
body.DbSchema = cast.Ptr("")
37+
return body
38+
}
39+
40+
// Convert Schemas to a comma-separated string
41+
if len(a.Schemas) > 0 {
42+
schemas := strings.Join(a.Schemas, ",")
43+
body.DbSchema = &schemas
44+
}
45+
46+
// Convert ExtraSearchPath to a comma-separated string
47+
if len(a.ExtraSearchPath) > 0 {
48+
extraSearchPath := strings.Join(a.ExtraSearchPath, ",")
49+
body.DbExtraSearchPath = &extraSearchPath
50+
}
51+
52+
// Convert MaxRows to int pointer
53+
if a.MaxRows > 0 {
54+
body.MaxRows = cast.Ptr(cast.UintToInt(a.MaxRows))
55+
}
56+
57+
// Note: DbPool is not present in the Api struct, so it's not set here
58+
return body
59+
}
60+
61+
func (a *api) fromRemoteApiConfig(remoteConfig v1API.PostgrestConfigWithJWTSecretResponse) api {
62+
result := *a
63+
if remoteConfig.DbSchema == "" {
64+
result.Enabled = false
65+
return result
66+
}
67+
68+
result.Enabled = true
69+
// Update Schemas if present in remoteConfig
70+
schemas := strings.Split(remoteConfig.DbSchema, ",")
71+
result.Schemas = make([]string, len(schemas))
72+
// TODO: use slices.Map when upgrade go version
73+
for i, schema := range schemas {
74+
result.Schemas[i] = strings.TrimSpace(schema)
75+
}
76+
77+
// Update ExtraSearchPath if present in remoteConfig
78+
extraSearchPath := strings.Split(remoteConfig.DbExtraSearchPath, ",")
79+
result.ExtraSearchPath = make([]string, len(extraSearchPath))
80+
for i, path := range extraSearchPath {
81+
result.ExtraSearchPath[i] = strings.TrimSpace(path)
82+
}
83+
84+
// Update MaxRows if present in remoteConfig
85+
result.MaxRows = cast.IntToUint(remoteConfig.MaxRows)
86+
87+
return result
88+
}
89+
90+
func (a *api) DiffWithRemote(remoteConfig v1API.PostgrestConfigWithJWTSecretResponse) ([]byte, error) {
91+
// Convert the config values into easily comparable remoteConfig values
92+
currentValue, err := ToTomlBytes(a)
93+
if err != nil {
94+
return nil, err
95+
}
96+
remoteCompare, err := ToTomlBytes(a.fromRemoteApiConfig(remoteConfig))
97+
if err != nil {
98+
return nil, err
99+
}
100+
return diff.Diff("remote[api]", remoteCompare, "local[api]", currentValue), nil
101+
}

pkg/config/api_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
v1API "github.com/supabase/cli/pkg/api"
8+
)
9+
10+
func TestApiToUpdatePostgrestConfigBody(t *testing.T) {
11+
t.Run("converts all fields correctly", func(t *testing.T) {
12+
api := &api{
13+
Enabled: true,
14+
Schemas: []string{"public", "private"},
15+
ExtraSearchPath: []string{"extensions", "public"},
16+
MaxRows: 1000,
17+
}
18+
19+
body := api.ToUpdatePostgrestConfigBody()
20+
21+
assert.Equal(t, "public,private", *body.DbSchema)
22+
assert.Equal(t, "extensions,public", *body.DbExtraSearchPath)
23+
assert.Equal(t, 1000, *body.MaxRows)
24+
})
25+
26+
t.Run("handles empty fields", func(t *testing.T) {
27+
api := &api{}
28+
29+
body := api.ToUpdatePostgrestConfigBody()
30+
31+
// remote api will be false by default, leading to an empty schema on api side
32+
assert.Equal(t, "", *body.DbSchema)
33+
})
34+
}
35+
36+
func TestApiDiffWithRemote(t *testing.T) {
37+
t.Run("detects differences", func(t *testing.T) {
38+
api := &api{
39+
Enabled: true,
40+
Schemas: []string{"public", "private"},
41+
ExtraSearchPath: []string{"extensions", "public"},
42+
MaxRows: 1000,
43+
}
44+
45+
remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
46+
DbSchema: "public",
47+
DbExtraSearchPath: "public",
48+
MaxRows: 500,
49+
}
50+
51+
diff, err := api.DiffWithRemote(remoteConfig)
52+
assert.NoError(t, err, string(diff))
53+
54+
assert.Contains(t, string(diff), "-schemas = [\"public\"]")
55+
assert.Contains(t, string(diff), "+schemas = [\"public\", \"private\"]")
56+
assert.Contains(t, string(diff), "-extra_search_path = [\"public\"]")
57+
assert.Contains(t, string(diff), "+extra_search_path = [\"extensions\", \"public\"]")
58+
assert.Contains(t, string(diff), "-max_rows = 500")
59+
assert.Contains(t, string(diff), "+max_rows = 1000")
60+
})
61+
62+
t.Run("handles no differences", func(t *testing.T) {
63+
api := &api{
64+
Enabled: true,
65+
Schemas: []string{"public"},
66+
ExtraSearchPath: []string{"public"},
67+
MaxRows: 500,
68+
}
69+
70+
remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
71+
DbSchema: "public",
72+
DbExtraSearchPath: "public",
73+
MaxRows: 500,
74+
}
75+
76+
diff, err := api.DiffWithRemote(remoteConfig)
77+
assert.NoError(t, err)
78+
79+
assert.Empty(t, diff)
80+
})
81+
82+
t.Run("handles multiple schemas and search paths with spaces", func(t *testing.T) {
83+
api := &api{
84+
Enabled: true,
85+
Schemas: []string{"public", "private"},
86+
ExtraSearchPath: []string{"extensions", "public"},
87+
MaxRows: 500,
88+
}
89+
90+
remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
91+
DbSchema: "public, private",
92+
DbExtraSearchPath: "extensions, public",
93+
MaxRows: 500,
94+
}
95+
96+
diff, err := api.DiffWithRemote(remoteConfig)
97+
assert.NoError(t, err)
98+
99+
assert.Empty(t, diff)
100+
})
101+
102+
t.Run("handles api disabled on remote side", func(t *testing.T) {
103+
api := &api{
104+
Enabled: true,
105+
Schemas: []string{"public", "private"},
106+
ExtraSearchPath: []string{"extensions", "public"},
107+
MaxRows: 500,
108+
}
109+
110+
remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
111+
DbSchema: "",
112+
DbExtraSearchPath: "",
113+
MaxRows: 0,
114+
}
115+
116+
diff, err := api.DiffWithRemote(remoteConfig)
117+
assert.NoError(t, err, string(diff))
118+
119+
assert.Contains(t, string(diff), "-enabled = false")
120+
assert.Contains(t, string(diff), "+enabled = true")
121+
})
122+
123+
t.Run("handles api disabled on local side", func(t *testing.T) {
124+
api := &api{
125+
Enabled: false,
126+
Schemas: []string{"public"},
127+
ExtraSearchPath: []string{"public"},
128+
MaxRows: 500,
129+
}
130+
131+
remoteConfig := v1API.PostgrestConfigWithJWTSecretResponse{
132+
DbSchema: "public",
133+
DbExtraSearchPath: "public",
134+
MaxRows: 500,
135+
}
136+
137+
diff, err := api.DiffWithRemote(remoteConfig)
138+
assert.NoError(t, err, string(diff))
139+
140+
assert.Contains(t, string(diff), "-enabled = true")
141+
assert.Contains(t, string(diff), "+enabled = false")
142+
})
143+
}

0 commit comments

Comments
 (0)