• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 19043045070

03 Nov 2025 05:11PM UTC coverage: 53.318% (-1.4%) from 54.728%
19043045070

Pull #4383

github

web-flow
Merge 321c707b4 into df557dc33
Pull Request #4383: feat(mcp): add `supabase mcp init` command to configure MCP clients

0 of 297 new or added lines in 8 files covered. (0.0%)

26 existing lines in 4 files now uncovered.

6388 of 11981 relevant lines covered (53.32%)

5.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.26
/internal/link/link.go
1
package link
2

3
import (
4
        "context"
5
        "fmt"
6
        "net/http"
7
        "os"
8
        "strconv"
9
        "strings"
10

11
        "github.com/go-errors/errors"
12
        "github.com/jackc/pgconn"
13
        "github.com/jackc/pgx/v4"
14
        "github.com/spf13/afero"
15
        "github.com/supabase/cli/internal/utils"
16
        "github.com/supabase/cli/internal/utils/flags"
17
        "github.com/supabase/cli/internal/utils/tenant"
18
        "github.com/supabase/cli/pkg/api"
19
        "github.com/supabase/cli/pkg/cast"
20
        cliConfig "github.com/supabase/cli/pkg/config"
21
        "github.com/supabase/cli/pkg/migration"
22
        "github.com/supabase/cli/pkg/queue"
23
)
24

25
func Run(ctx context.Context, projectRef string, skipPooler bool, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
3✔
26
        majorVersion := utils.Config.Db.MajorVersion
3✔
27
        if err := checkRemoteProjectStatus(ctx, projectRef, fsys); err != nil {
3✔
28
                return err
×
29
        }
×
30

31
        // 1. Check service config
32
        keys, err := tenant.GetApiKeys(ctx, projectRef)
3✔
33
        if err != nil {
3✔
34
                return err
×
35
        }
×
36
        LinkServices(ctx, projectRef, keys.ServiceRole, skipPooler, fsys)
3✔
37

3✔
38
        // 2. Check database connection
3✔
39
        config := flags.NewDbConfigWithPassword(ctx, projectRef)
3✔
40
        if err := linkDatabase(ctx, config, fsys, options...); err != nil {
4✔
41
                return err
1✔
42
        }
1✔
43

44
        // 3. Save project ref
45
        if err := utils.WriteFile(utils.ProjectRefPath, []byte(projectRef), fsys); err != nil {
3✔
46
                return err
1✔
47
        }
1✔
48
        fmt.Fprintln(os.Stdout, "Finished "+utils.Aqua("supabase link")+".")
1✔
49

1✔
50
        // 4. Suggest config update
1✔
51
        if utils.Config.Db.MajorVersion != majorVersion {
2✔
52
                fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local database version differs from the linked project.")
1✔
53
                fmt.Fprintf(os.Stderr, `Update your %s to fix it:
1✔
54
[db]
1✔
55
major_version = %d
1✔
56
`, utils.Bold(utils.ConfigPath), utils.Config.Db.MajorVersion)
1✔
57
        }
1✔
58
        return nil
1✔
59
}
60

61
func LinkServices(ctx context.Context, projectRef, serviceKey string, skipPooler bool, fsys afero.Fs) {
3✔
62
        jq := queue.NewJobQueue(5)
3✔
63
        api := tenant.NewTenantAPI(ctx, projectRef, serviceKey)
3✔
64
        jobs := []func() error{
3✔
65
                func() error { return linkDatabaseSettings(ctx, projectRef) },
6✔
66
                func() error { return linkNetworkRestrictions(ctx, projectRef) },
3✔
67
                func() error { return linkPostgrest(ctx, projectRef) },
3✔
68
                func() error { return linkGotrue(ctx, projectRef) },
3✔
69
                func() error { return linkStorage(ctx, projectRef) },
3✔
70
                func() error {
3✔
71
                        if skipPooler {
3✔
72
                                utils.Config.Db.Pooler.ConnectionString = ""
×
73
                                return fsys.RemoveAll(utils.PoolerUrlPath)
×
UNCOV
74
                        }
×
75
                        return linkPooler(ctx, projectRef, fsys)
3✔
76
                },
77
                func() error { return linkPostgrestVersion(ctx, api, fsys) },
3✔
78
                func() error { return linkGotrueVersion(ctx, api, fsys) },
3✔
79
                func() error { return linkStorageVersion(ctx, api, fsys) },
3✔
80
        }
81
        // Ignore non-fatal errors linking services
82
        logger := utils.GetDebugLogger()
3✔
83
        for _, job := range jobs {
30✔
84
                if err := jq.Put(job); err != nil {
36✔
85
                        fmt.Fprintln(logger, err)
9✔
86
                }
9✔
87
        }
88
        if err := jq.Collect(); err != nil {
5✔
89
                fmt.Fprintln(logger, err)
2✔
90
        }
2✔
91
}
92

93
func linkPostgrest(ctx context.Context, projectRef string) error {
7✔
94
        resp, err := utils.GetSupabase().V1GetPostgrestServiceConfigWithResponse(ctx, projectRef)
7✔
95
        if err != nil {
10✔
96
                return errors.Errorf("failed to read API config: %w", err)
3✔
97
        } else if resp.JSON200 == nil {
8✔
98
                return errors.Errorf("unexpected API config status %d: %s", resp.StatusCode(), string(resp.Body))
1✔
99
        }
1✔
100
        utils.Config.Api.FromRemoteApiConfig(*resp.JSON200)
3✔
101
        return nil
3✔
102
}
103

104
func linkPostgrestVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
105
        version, err := api.GetPostgrestVersion(ctx)
3✔
106
        if err != nil {
5✔
107
                return err
2✔
108
        }
2✔
109
        return utils.WriteFile(utils.RestVersionPath, []byte(version), fsys)
1✔
110
}
111

112
func linkGotrue(ctx context.Context, projectRef string) error {
3✔
113
        resp, err := utils.GetSupabase().V1GetAuthServiceConfigWithResponse(ctx, projectRef)
3✔
114
        if err != nil {
6✔
115
                return errors.Errorf("failed to read Auth config: %w", err)
3✔
116
        } else if resp.JSON200 == nil {
3✔
117
                return errors.Errorf("unexpected Auth config status %d: %s", resp.StatusCode(), string(resp.Body))
×
118
        }
×
119
        utils.Config.Auth.FromRemoteAuthConfig(*resp.JSON200)
×
UNCOV
120
        return nil
×
121
}
122

123
func linkGotrueVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
124
        version, err := api.GetGotrueVersion(ctx)
3✔
125
        if err != nil {
5✔
126
                return err
2✔
127
        }
2✔
128
        return utils.WriteFile(utils.GotrueVersionPath, []byte(version), fsys)
1✔
129
}
130

131
func linkStorage(ctx context.Context, projectRef string) error {
3✔
132
        resp, err := utils.GetSupabase().V1GetStorageConfigWithResponse(ctx, projectRef)
3✔
133
        if err != nil {
5✔
134
                return errors.Errorf("failed to read Storage config: %w", err)
2✔
135
        } else if resp.JSON200 == nil {
3✔
136
                return errors.Errorf("unexpected Storage config status %d: %s", resp.StatusCode(), string(resp.Body))
×
UNCOV
137
        }
×
138
        utils.Config.Storage.FromRemoteStorageConfig(*resp.JSON200)
1✔
139
        return nil
1✔
140
}
141

142
func linkStorageVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
143
        version, err := api.GetStorageVersion(ctx)
3✔
144
        if err != nil {
5✔
145
                return err
2✔
146
        }
2✔
147
        return utils.WriteFile(utils.StorageVersionPath, []byte(version), fsys)
1✔
148
}
149

150
const GET_LATEST_STORAGE_MIGRATION = "SELECT name FROM storage.migrations ORDER BY id DESC LIMIT 1"
151

152
func linkStorageMigration(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
5✔
153
        var name string
5✔
154
        if err := conn.QueryRow(ctx, GET_LATEST_STORAGE_MIGRATION).Scan(&name); err != nil {
6✔
155
                return errors.Errorf("failed to fetch storage migration: %w", err)
1✔
156
        }
1✔
157
        return utils.WriteFile(utils.StorageMigrationPath, []byte(name), fsys)
4✔
158
}
159

160
func linkDatabaseSettings(ctx context.Context, projectRef string) error {
3✔
161
        resp, err := utils.GetSupabase().V1GetPostgresConfigWithResponse(ctx, projectRef)
3✔
162
        if err != nil {
5✔
163
                return errors.Errorf("failed to read DB config: %w", err)
2✔
164
        } else if resp.JSON200 == nil {
3✔
165
                return errors.Errorf("unexpected DB config status %d: %s", resp.StatusCode(), string(resp.Body))
×
UNCOV
166
        }
×
167
        utils.Config.Db.Settings.FromRemotePostgresConfig(*resp.JSON200)
1✔
168
        return nil
1✔
169
}
170

171
func linkNetworkRestrictions(ctx context.Context, projectRef string) error {
3✔
172
        resp, err := utils.GetSupabase().V1GetNetworkRestrictionsWithResponse(ctx, projectRef)
3✔
173
        if err != nil {
3✔
UNCOV
174
                return errors.Errorf("failed to read network restrictions: %w", err)
×
175
        } else if resp.JSON200 == nil {
3✔
176
                return errors.Errorf("unexpected network restrictions status %d: %s", resp.StatusCode(), string(resp.Body))
×
UNCOV
177
        }
×
178
        utils.Config.Db.NetworkRestrictions.FromRemoteNetworkRestrictions(*resp.JSON200)
3✔
179
        return nil
3✔
180
}
181

182
func linkDatabase(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
7✔
183
        conn, err := utils.ConnectByConfig(ctx, config, options...)
7✔
184
        if err != nil {
9✔
185
                return err
2✔
186
        }
2✔
187
        defer conn.Close(context.Background())
5✔
188
        updatePostgresConfig(conn)
5✔
189
        if err := linkStorageMigration(ctx, conn, fsys); err != nil {
7✔
190
                fmt.Fprintln(os.Stderr, err)
2✔
191
        }
2✔
192
        // If `schema_migrations` doesn't exist on the remote database, create it.
193
        if err := migration.CreateMigrationTable(ctx, conn); err != nil {
6✔
194
                return err
1✔
195
        }
1✔
196
        return migration.CreateSeedTable(ctx, conn)
4✔
197
}
198

199
func updatePostgresConfig(conn *pgx.Conn) {
5✔
200
        serverVersion := conn.PgConn().ParameterStatus("server_version")
5✔
201
        // Safe to assume that supported Postgres version is 10.0 <= n < 100.0
5✔
202
        majorDigits := min(len(serverVersion), 2)
5✔
203
        // Treat error as unchanged
5✔
204
        if dbMajorVersion, err := strconv.ParseUint(serverVersion[:majorDigits], 10, 7); err == nil {
9✔
205
                utils.Config.Db.MajorVersion = uint(dbMajorVersion)
4✔
206
        }
4✔
207
}
208

209
func linkPooler(ctx context.Context, projectRef string, fsys afero.Fs) error {
3✔
210
        resp, err := utils.GetSupabase().V1GetPoolerConfigWithResponse(ctx, projectRef)
3✔
211
        if err != nil {
6✔
212
                return errors.Errorf("failed to get pooler config: %w", err)
3✔
213
        }
3✔
214
        if resp.JSON200 == nil {
×
215
                return errors.Errorf("%w: %s", tenant.ErrAuthToken, string(resp.Body))
×
216
        }
×
217
        for _, config := range *resp.JSON200 {
×
218
                if config.DatabaseType == api.SupavisorConfigResponseDatabaseTypePRIMARY {
×
219
                        updatePoolerConfig(config)
×
UNCOV
220
                }
×
221
        }
UNCOV
222
        return utils.WriteFile(utils.PoolerUrlPath, []byte(utils.Config.Db.Pooler.ConnectionString), fsys)
×
223
}
224

225
func updatePoolerConfig(config api.SupavisorConfigResponse) {
×
226
        // Remove password from pooler connection string because the placeholder text
×
227
        // [YOUR-PASSWORD] messes up pgconn.ParseConfig. The password must be percent
×
228
        // escaped so we cannot simply call strings.Replace with actual password.
×
229
        utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(config.ConnectionString, ":[YOUR-PASSWORD]", "")
×
230
        // Always use session mode for running migrations
×
231
        if utils.Config.Db.Pooler.PoolMode = cliConfig.SessionMode; config.PoolMode != api.SupavisorConfigResponsePoolModeSession {
×
232
                utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(utils.Config.Db.Pooler.ConnectionString, ":6543/", ":5432/")
×
233
        }
×
234
        if value, err := config.DefaultPoolSize.Get(); err == nil {
×
235
                utils.Config.Db.Pooler.DefaultPoolSize = cast.IntToUint(value)
×
236
        }
×
237
        if value, err := config.MaxClientConn.Get(); err == nil {
×
238
                utils.Config.Db.Pooler.MaxClientConn = cast.IntToUint(value)
×
UNCOV
239
        }
×
240
}
241

242
var errProjectPaused = errors.New("project is paused")
243

244
func checkRemoteProjectStatus(ctx context.Context, projectRef string, fsys afero.Fs) error {
6✔
245
        resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, projectRef)
6✔
246
        if err != nil {
6✔
247
                return errors.Errorf("failed to retrieve remote project status: %w", err)
×
UNCOV
248
        }
×
249
        switch resp.StatusCode() {
6✔
250
        case http.StatusNotFound:
1✔
251
                // Ignore not found error to support linking branch projects
1✔
252
                return nil
1✔
253
        case http.StatusOK:
5✔
254
                // resp.JSON200 is not nil, proceed
255
        default:
×
UNCOV
256
                return errors.New("Unexpected error retrieving remote project status: " + string(resp.Body))
×
257
        }
258

259
        switch resp.JSON200.Status {
5✔
260
        case api.V1ProjectWithDatabaseResponseStatusINACTIVE:
1✔
261
                utils.CmdSuggestion = fmt.Sprintf("An admin must unpause it from the Supabase dashboard at %s", utils.Aqua(fmt.Sprintf("%s/project/%s", utils.GetSupabaseDashboardURL(), projectRef)))
1✔
262
                return errors.New(errProjectPaused)
1✔
263
        case api.V1ProjectWithDatabaseResponseStatusACTIVEHEALTHY:
4✔
264
                // Project is in the desired state, do nothing
265
        default:
×
UNCOV
266
                fmt.Fprintf(os.Stderr, "%s: Project status is %s instead of Active Healthy. Some operations might fail.\n", utils.Yellow("WARNING"), resp.JSON200.Status)
×
267
        }
268

269
        // Update postgres image version to match the remote project
270
        if version := resp.JSON200.Database.Version; len(version) > 0 {
6✔
271
                return utils.WriteFile(utils.PostgresVersionPath, []byte(version), fsys)
2✔
272
        }
2✔
273
        return nil
2✔
274
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc