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

supabase / cli / 19325748827

13 Nov 2025 08:49AM UTC coverage: 54.419% (-0.3%) from 54.699%
19325748827

Pull #4372

github

web-flow
Merge e70f78d92 into a057b7430
Pull Request #4372: fix: toggle `DENO_NO_PACKAGE_JSON` conditionally

3 of 3 new or added lines in 1 file covered. (100.0%)

160 existing lines in 12 files now uncovered.

6361 of 11689 relevant lines covered (54.42%)

6.1 hits per line

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

73.8
/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/queue"
22
)
23

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

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

3✔
37
        // 2. Check database connection
3✔
38
        if config, err := flags.NewDbConfigWithPassword(ctx, projectRef); err != nil {
6✔
39
                fmt.Fprintln(os.Stderr, utils.Yellow("WARN:"), err)
3✔
40
        } else if err := linkDatabase(ctx, config, fsys, options...); err != nil {
3✔
UNCOV
41
                return err
×
UNCOV
42
        }
×
43

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

2✔
50
        // 4. Suggest config update
2✔
51
        if utils.Config.Db.MajorVersion != majorVersion {
2✔
UNCOV
52
                fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local database version differs from the linked project.")
×
UNCOV
53
                fmt.Fprintf(os.Stderr, `Update your %s to fix it:
×
UNCOV
54
[db]
×
UNCOV
55
major_version = %d
×
UNCOV
56
`, utils.Bold(utils.ConfigPath), utils.Config.Db.MajorVersion)
×
UNCOV
57
        }
×
58
        return nil
2✔
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)
×
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 {
34✔
85
                        fmt.Fprintln(logger, err)
7✔
86
                }
7✔
87
        }
88
        if err := jq.Collect(); err != nil {
6✔
89
                fmt.Fprintln(logger, err)
3✔
90
        }
3✔
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)
×
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))
×
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 {
4✔
153
        var name string
4✔
154
        if err := conn.QueryRow(ctx, GET_LATEST_STORAGE_MIGRATION).Scan(&name); err != nil {
5✔
155
                return errors.Errorf("failed to fetch storage migration: %w", err)
1✔
156
        }
1✔
157
        return utils.WriteFile(utils.StorageMigrationPath, []byte(name), fsys)
3✔
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))
×
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✔
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))
×
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 {
5✔
183
        conn, err := utils.ConnectByConfig(ctx, config, options...)
5✔
184
        if err != nil {
6✔
185
                return err
1✔
186
        }
1✔
187
        defer conn.Close(context.Background())
4✔
188
        updatePostgresConfig(conn)
4✔
189
        return linkStorageMigration(ctx, conn, fsys)
4✔
190
}
191

192
func updatePostgresConfig(conn *pgx.Conn) {
4✔
193
        serverVersion := conn.PgConn().ParameterStatus("server_version")
4✔
194
        // Safe to assume that supported Postgres version is 10.0 <= n < 100.0
4✔
195
        majorDigits := min(len(serverVersion), 2)
4✔
196
        // Treat error as unchanged
4✔
197
        if dbMajorVersion, err := strconv.ParseUint(serverVersion[:majorDigits], 10, 7); err == nil {
7✔
198
                utils.Config.Db.MajorVersion = uint(dbMajorVersion)
3✔
199
        }
3✔
200
}
201

202
func linkPooler(ctx context.Context, projectRef string, fsys afero.Fs) error {
3✔
203
        primary, err := utils.GetPoolerConfigPrimary(ctx, projectRef)
3✔
204
        if err != nil {
6✔
205
                return err
3✔
206
        }
3✔
UNCOV
207
        updatePoolerConfig(primary)
×
UNCOV
208
        return utils.WriteFile(utils.PoolerUrlPath, []byte(utils.Config.Db.Pooler.ConnectionString), fsys)
×
209
}
210

UNCOV
211
func updatePoolerConfig(config api.SupavisorConfigResponse) {
×
UNCOV
212
        // Remove password from pooler connection string because the placeholder text
×
UNCOV
213
        // [YOUR-PASSWORD] messes up pgconn.ParseConfig. The password must be percent
×
214
        // escaped so we cannot simply call strings.Replace with actual password.
×
215
        utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(config.ConnectionString, ":[YOUR-PASSWORD]", "")
×
216
        // Always use session mode for running migrations
×
217
        if utils.Config.Db.Pooler.PoolMode = cliConfig.SessionMode; config.PoolMode != api.SupavisorConfigResponsePoolModeSession {
×
218
                utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(utils.Config.Db.Pooler.ConnectionString, ":6543/", ":5432/")
×
219
        }
×
220
        if value, err := config.DefaultPoolSize.Get(); err == nil {
×
UNCOV
221
                utils.Config.Db.Pooler.DefaultPoolSize = cast.IntToUint(value)
×
222
        }
×
UNCOV
223
        if value, err := config.MaxClientConn.Get(); err == nil {
×
UNCOV
224
                utils.Config.Db.Pooler.MaxClientConn = cast.IntToUint(value)
×
225
        }
×
226
}
227

228
var errProjectPaused = errors.New("project is paused")
229

230
func checkRemoteProjectStatus(ctx context.Context, projectRef string, fsys afero.Fs) error {
6✔
231
        resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, projectRef)
6✔
232
        if err != nil {
6✔
233
                return errors.Errorf("failed to retrieve remote project status: %w", err)
×
234
        }
×
235
        switch resp.StatusCode() {
6✔
236
        case http.StatusNotFound:
1✔
237
                // Ignore not found error to support linking branch projects
1✔
238
                return nil
1✔
239
        case http.StatusOK:
5✔
240
                // resp.JSON200 is not nil, proceed
UNCOV
241
        default:
×
UNCOV
242
                return errors.New("Unexpected error retrieving remote project status: " + string(resp.Body))
×
243
        }
244

245
        switch resp.JSON200.Status {
5✔
246
        case api.V1ProjectWithDatabaseResponseStatusINACTIVE:
1✔
247
                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✔
248
                return errors.New(errProjectPaused)
1✔
249
        case api.V1ProjectWithDatabaseResponseStatusACTIVEHEALTHY:
4✔
250
                // Project is in the desired state, do nothing
UNCOV
251
        default:
×
UNCOV
252
                fmt.Fprintf(os.Stderr, "%s: Project status is %s instead of Active Healthy. Some operations might fail.\n", utils.Yellow("WARNING"), resp.JSON200.Status)
×
253
        }
254

255
        // Update postgres image version to match the remote project
256
        if version := resp.JSON200.Database.Version; len(version) > 0 {
6✔
257
                return utils.WriteFile(utils.PostgresVersionPath, []byte(version), fsys)
2✔
258
        }
2✔
259
        return nil
2✔
260
}
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