From 286bb75b28a384b1b43c72b8230fa9b15a2a95f7 Mon Sep 17 00:00:00 2001 From: "Idris M. Celik" Date: Sun, 15 Jun 2025 11:21:47 +0000 Subject: [PATCH 1/4] Fix #3719 (Add --prune option to 'supabase functions deploy' to remove orphaned functions) --- cmd/functions.go | 6 +- internal/functions/deploy/deploy.go | 90 ++++++++++++- internal/functions/deploy/deploy_test.go | 160 +++++++++++++++++++++-- 3 files changed, 246 insertions(+), 10 deletions(-) diff --git a/cmd/functions.go b/cmd/functions.go index cc5ffa53de..531a7febc7 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -58,6 +58,8 @@ var ( useLegacyBundle bool noVerifyJWT = new(bool) importMapPath string + prune bool + force bool functionsDeployCmd = &cobra.Command{ Use: "deploy [Function name]", @@ -73,7 +75,7 @@ var ( } else if maxJobs > 1 { return errors.New("--jobs must be used together with --use-api") } - return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, afero.NewOsFs()) + return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, force, afero.NewOsFs()) }, } @@ -139,6 +141,8 @@ func init() { cobra.CheckErr(deployFlags.MarkHidden("legacy-bundle")) deployFlags.UintVarP(&maxJobs, "jobs", "j", 1, "Maximum number of parallel jobs.") deployFlags.BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") + deployFlags.BoolVar(&prune, "prune", false, "Delete Functions that exist in Supabase but not in local project.") + deployFlags.BoolVar(&force, "force", false, "Disable confirmation prompts when used with --prune.") deployFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") deployFlags.StringVar(&importMapPath, "import-map", "", "Path to import map file.") functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 470a543040..8d6c16e884 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -15,7 +15,7 @@ import ( "github.com/supabase/cli/pkg/function" ) -func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, fsys afero.Fs) error { +func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune bool, force bool, fsys afero.Fs) error { // Load function config and project id if err := flags.LoadConfig(fsys); err != nil { return err @@ -64,6 +64,14 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, } else if err != nil { return err } + + // Handle pruning if requested + if prune { + if err := pruneFunctions(ctx, slugs, flags.ProjectRef, force); err != nil { + return err + } + } + fmt.Printf("Deployed Functions on project %s: %s\n", utils.Aqua(flags.ProjectRef), strings.Join(slugs, ", ")) url := fmt.Sprintf("%s/project/%v/functions", utils.GetSupabaseDashboardURL(), flags.ProjectRef) fmt.Println("You can inspect your deployment in the Dashboard: " + url) @@ -89,6 +97,86 @@ func GetFunctionSlugs(fsys afero.Fs) (slugs []string, err error) { return slugs, nil } +// getRemoteFunctions gets the list of functions from the Supabase project +func getRemoteFunctions(ctx context.Context, projectRef string) ([]string, error) { + resp, err := utils.GetSupabase().V1ListAllFunctionsWithResponse(ctx, projectRef) + if err != nil { + return nil, errors.Errorf("failed to list remote functions: %w", err) + } + + if resp.JSON200 == nil { + return nil, errors.New("Unexpected error retrieving functions: " + string(resp.Body)) + } + + var remoteSlugs []string + for _, function := range *resp.JSON200 { + remoteSlugs = append(remoteSlugs, function.Slug) + } + + return remoteSlugs, nil +} + +// pruneFunctions deletes functions that exist remotely but not locally +func pruneFunctions(ctx context.Context, localSlugs []string, projectRef string, force bool) error { + // Get remote functions + remoteSlugs, err := getRemoteFunctions(ctx, projectRef) + if err != nil { + return err + } + + // Create a set of local function slugs for fast lookup + localSet := make(map[string]bool) + for _, slug := range localSlugs { + localSet[slug] = true + } + + // Find functions to prune (exist remotely but not locally) + var functionsToDelete []string + for _, remoteSlug := range remoteSlugs { + if !localSet[remoteSlug] { + functionsToDelete = append(functionsToDelete, remoteSlug) + } + } + + // If no functions to prune, return early + if len(functionsToDelete) == 0 { + fmt.Fprintln(os.Stderr, "No functions to prune.") + return nil + } + + // In interactive mode, prompt for confirmation (unless force is used) + if !force && utils.NewConsole().IsTTY { + message := fmt.Sprintf("The following functions will be DELETED from your project: %s", strings.Join(functionsToDelete, ", ")) + fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), message) + + if confirmed, err := utils.NewConsole().PromptYesNo(ctx, "Are you sure?", false); err != nil { + return err + } else if !confirmed { + fmt.Fprintln(os.Stderr, "Aborted.") + return nil + } + } + + // Delete functions + for _, slug := range functionsToDelete { + fmt.Fprintf(os.Stderr, "Deleting function: %s\n", utils.Aqua(slug)) + resp, err := utils.GetSupabase().V1DeleteAFunctionWithResponse(ctx, projectRef, slug) + if err != nil { + return errors.Errorf("failed to delete function %s: %w", slug, err) + } + switch resp.StatusCode() { + case 404: + fmt.Fprintf(os.Stderr, "Function %s was already deleted.\n", utils.Aqua(slug)) + case 200: + fmt.Fprintf(os.Stderr, "Successfully deleted function: %s\n", utils.Aqua(slug)) + default: + return errors.Errorf("failed to delete function %s: %s", slug, string(resp.Body)) + } + } + + return nil +} + func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) (config.FunctionConfig, error) { // Although some functions do not require import map, it's more convenient to setup // vscode deno extension with a single import map for all functions. diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index 9f0fb79dbf..45bbe9b537 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -74,7 +74,7 @@ func TestDeployCommand(t *testing.T) { } // Run test noVerifyJWT := true - err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, fsys) + err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, false, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -129,7 +129,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, true, nil, "", 1, fsys) + err = Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -182,7 +182,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, ".output_enabled-func") require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, true, nil, "", 1, fsys) + err = Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -193,7 +193,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, fsys) + err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, false, false, fsys) // Check error assert.ErrorContains(t, err, "Invalid Function name.") }) @@ -203,7 +203,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), nil, true, nil, "", 1, fsys) + err := Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) // Check error assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions") }) @@ -249,7 +249,7 @@ verify_jwt = false outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, false, false, fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -295,8 +295,8 @@ verify_jwt = false outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - noVerifyJwt := false - assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJwt, "", 1, fsys)) + noVerifyJWT := false + assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJWT, "", 1, false, false, fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -372,3 +372,147 @@ func TestImportMapPath(t *testing.T) { assert.Equal(t, path, fc["test"].ImportMap) }) } + +func TestPruneFunctions(t *testing.T) { + flags.ProjectRef = apitest.RandomProjectRef() + + t.Run("prunes functions not in local directory", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + require.NoError(t, utils.WriteConfig(fsys, false)) + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + // Setup function entrypoints + localFunctions := []string{"local-func-1", "local-func-2"} + for _, fn := range localFunctions { + entrypointPath := filepath.Join(utils.FunctionsDir, fn, "index.ts") + require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + } + // Setup mock api - remote functions include local ones plus orphaned ones + defer gock.OffAll() + remoteFunctions := []api.FunctionResponse{ + {Slug: "local-func-1", Id: "1", Name: "local-func-1"}, + {Slug: "local-func-2", Id: "2", Name: "local-func-2"}, + {Slug: "orphaned-func-1", Id: "3", Name: "orphaned-func-1"}, + {Slug: "orphaned-func-2", Id: "4", Name: "orphaned-func-2"}, + } + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON(remoteFunctions) + // Mock deploy endpoint for local functions + for _, fn := range localFunctions { + gock.New(utils.DefaultApiHost). + Post("/v1/projects/"+flags.ProjectRef+"/functions"). + MatchParam("slug", fn). + Reply(http.StatusCreated). + JSON(api.FunctionResponse{Id: fn}) + } + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON(api.BulkUpdateFunctionResponse{}) + // Mock delete endpoints for orphaned functions + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/functions/orphaned-func-1"). + Reply(http.StatusOK) + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/functions/orphaned-func-2"). + Reply(http.StatusOK) + + // Run test with prune and force (to skip confirmation) + err := Run(context.Background(), nil, false, nil, "", 1, true, true, fsys) + // Check error + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("skips pruning when no orphaned functions", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + require.NoError(t, utils.WriteConfig(fsys, false)) + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + // Setup function entrypoints + localFunctions := []string{"local-func-1", "local-func-2"} + for _, fn := range localFunctions { + entrypointPath := filepath.Join(utils.FunctionsDir, fn, "index.ts") + require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + } + // Setup mock api - remote functions match local ones exactly + defer gock.OffAll() + remoteFunctions := []api.FunctionResponse{ + {Slug: "local-func-1", Id: "1", Name: "local-func-1"}, + {Slug: "local-func-2", Id: "2", Name: "local-func-2"}, + } + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON(remoteFunctions) + // Mock deploy endpoint for local functions + for _, fn := range localFunctions { + gock.New(utils.DefaultApiHost). + Post("/v1/projects/"+flags.ProjectRef+"/functions"). + MatchParam("slug", fn). + Reply(http.StatusCreated). + JSON(api.FunctionResponse{Id: fn}) + } + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON(api.BulkUpdateFunctionResponse{}) + + // Run test with prune and force + err := Run(context.Background(), nil, false, nil, "", 1, true, true, fsys) + // Check error + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("handles 404 on delete gracefully", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + require.NoError(t, utils.WriteConfig(fsys, false)) + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + // Setup function entrypoints + localFunctions := []string{"local-func"} + for _, fn := range localFunctions { + entrypointPath := filepath.Join(utils.FunctionsDir, fn, "index.ts") + require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + } + // Setup mock api + defer gock.OffAll() + remoteFunctions := []api.FunctionResponse{ + {Slug: "local-func", Id: "1", Name: "local-func"}, + {Slug: "orphaned-func", Id: "2", Name: "orphaned-func"}, + } + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON(remoteFunctions) + // Mock deploy endpoint + gock.New(utils.DefaultApiHost). + Post("/v1/projects/"+flags.ProjectRef+"/functions"). + MatchParam("slug", "local-func"). + Reply(http.StatusCreated). + JSON(api.FunctionResponse{Id: "local-func"}) + gock.New(utils.DefaultApiHost). + Put("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON(api.BulkUpdateFunctionResponse{}) + // Mock delete endpoint with 404 (function already deleted) + gock.New(utils.DefaultApiHost). + Delete("/v1/projects/" + flags.ProjectRef + "/functions/orphaned-func"). + Reply(http.StatusNotFound) + + // Run test with prune and force + err := Run(context.Background(), nil, false, nil, "", 1, true, true, fsys) + // Check error + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) +} From bd550b0dba835e2ecbc73332353e1edd07428ad2 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Thu, 26 Jun 2025 00:09:47 +0800 Subject: [PATCH 2/4] chore: address PR comments --- cmd/functions.go | 4 +- cmd/root.go | 1 + internal/functions/delete/delete.go | 26 ++-- internal/functions/delete/delete_test.go | 4 +- internal/functions/deploy/deploy.go | 145 +++++++++-------------- internal/functions/deploy/deploy_test.go | 22 ++-- internal/utils/console.go | 5 + 7 files changed, 90 insertions(+), 117 deletions(-) diff --git a/cmd/functions.go b/cmd/functions.go index 531a7febc7..9c542844a3 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -59,7 +59,6 @@ var ( noVerifyJWT = new(bool) importMapPath string prune bool - force bool functionsDeployCmd = &cobra.Command{ Use: "deploy [Function name]", @@ -75,7 +74,7 @@ var ( } else if maxJobs > 1 { return errors.New("--jobs must be used together with --use-api") } - return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, force, afero.NewOsFs()) + return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, afero.NewOsFs()) }, } @@ -142,7 +141,6 @@ func init() { deployFlags.UintVarP(&maxJobs, "jobs", "j", 1, "Maximum number of parallel jobs.") deployFlags.BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") deployFlags.BoolVar(&prune, "prune", false, "Delete Functions that exist in Supabase but not in local project.") - deployFlags.BoolVar(&force, "force", false, "Disable confirmation prompts when used with --prune.") deployFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") deployFlags.StringVar(&importMapPath, "import-map", "", "Path to import map file.") functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") diff --git a/cmd/root.go b/cmd/root.go index a2cc6f0a7f..d053045e89 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -232,6 +232,7 @@ func init() { }) flags := rootCmd.PersistentFlags() + flags.Bool("yes", false, "answer yes to all prompts") flags.Bool("debug", false, "output debug logs to stderr") flags.String("workdir", "", "path to a Supabase project directory") flags.Bool("experimental", false, "enable experimental features") diff --git a/internal/functions/delete/delete.go b/internal/functions/delete/delete.go index 47d5957efa..5e087e71e1 100644 --- a/internal/functions/delete/delete.go +++ b/internal/functions/delete/delete.go @@ -11,27 +11,29 @@ import ( ) func Run(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error { - // 1. Sanity checks. - { - if err := utils.ValidateFunctionSlug(slug); err != nil { - return err - } + if err := utils.ValidateFunctionSlug(slug); err != nil { + return err } + if err := Undeploy(ctx, projectRef, slug); err != nil { + return err + } + fmt.Printf("Deleted Function %s from project %s.\n", utils.Aqua(slug), utils.Aqua(projectRef)) + return nil +} - // 2. Delete Function. +var ErrNoDelete = errors.New("nothing to delete") + +func Undeploy(ctx context.Context, projectRef string, slug string) error { resp, err := utils.GetSupabase().V1DeleteAFunctionWithResponse(ctx, projectRef, slug) if err != nil { return errors.Errorf("failed to delete function: %w", err) } switch resp.StatusCode() { case http.StatusNotFound: - return errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.") + return errors.Errorf("Function %s does not exist on the Supabase project: %w", slug, ErrNoDelete) case http.StatusOK: - break + return nil default: - return errors.New("Failed to delete Function " + utils.Aqua(slug) + " on the Supabase project: " + string(resp.Body)) + return errors.Errorf("unexpected delete function status %d: %s", resp.StatusCode(), string(resp.Body)) } - - fmt.Println("Deleted Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".") - return nil } diff --git a/internal/functions/delete/delete_test.go b/internal/functions/delete/delete_test.go index 110208ea90..79bd2bb5ac 100644 --- a/internal/functions/delete/delete_test.go +++ b/internal/functions/delete/delete_test.go @@ -73,7 +73,7 @@ func TestDeleteCommand(t *testing.T) { // Run test err := Run(context.Background(), slug, project, fsys) // Check error - assert.ErrorContains(t, err, "Function test-func does not exist on the Supabase project.") + assert.ErrorIs(t, err, ErrNoDelete) assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -88,7 +88,7 @@ func TestDeleteCommand(t *testing.T) { // Run test err := Run(context.Background(), slug, project, fsys) // Check error - assert.ErrorContains(t, err, "Failed to delete Function test-func on the Supabase project:") + assert.ErrorContains(t, err, "unexpected delete function status 503:") assert.Empty(t, apitest.ListUnmatchedRequests()) }) } diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 8d6c16e884..329678a16e 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -9,13 +9,15 @@ import ( "github.com/go-errors/errors" "github.com/spf13/afero" + "github.com/supabase/cli/internal/functions/delete" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/config" "github.com/supabase/cli/pkg/function" ) -func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune bool, force bool, fsys afero.Fs) error { +func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune bool, fsys afero.Fs) error { // Load function config and project id if err := flags.LoadConfig(fsys); err != nil { return err @@ -49,6 +51,7 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, if err != nil { return err } + // Deploy new and updated functions opt := function.WithMaxJobs(maxJobs) if useDocker { if utils.IsDockerRunning(ctx) { @@ -64,18 +67,13 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, } else if err != nil { return err } - - // Handle pruning if requested - if prune { - if err := pruneFunctions(ctx, slugs, flags.ProjectRef, force); err != nil { - return err - } - } - fmt.Printf("Deployed Functions on project %s: %s\n", utils.Aqua(flags.ProjectRef), strings.Join(slugs, ", ")) url := fmt.Sprintf("%s/project/%v/functions", utils.GetSupabaseDashboardURL(), flags.ProjectRef) fmt.Println("You can inspect your deployment in the Dashboard: " + url) - return nil + if !prune { + return nil + } + return pruneFunctions(ctx, functionConfig) } func GetFunctionSlugs(fsys afero.Fs) (slugs []string, err error) { @@ -97,86 +95,6 @@ func GetFunctionSlugs(fsys afero.Fs) (slugs []string, err error) { return slugs, nil } -// getRemoteFunctions gets the list of functions from the Supabase project -func getRemoteFunctions(ctx context.Context, projectRef string) ([]string, error) { - resp, err := utils.GetSupabase().V1ListAllFunctionsWithResponse(ctx, projectRef) - if err != nil { - return nil, errors.Errorf("failed to list remote functions: %w", err) - } - - if resp.JSON200 == nil { - return nil, errors.New("Unexpected error retrieving functions: " + string(resp.Body)) - } - - var remoteSlugs []string - for _, function := range *resp.JSON200 { - remoteSlugs = append(remoteSlugs, function.Slug) - } - - return remoteSlugs, nil -} - -// pruneFunctions deletes functions that exist remotely but not locally -func pruneFunctions(ctx context.Context, localSlugs []string, projectRef string, force bool) error { - // Get remote functions - remoteSlugs, err := getRemoteFunctions(ctx, projectRef) - if err != nil { - return err - } - - // Create a set of local function slugs for fast lookup - localSet := make(map[string]bool) - for _, slug := range localSlugs { - localSet[slug] = true - } - - // Find functions to prune (exist remotely but not locally) - var functionsToDelete []string - for _, remoteSlug := range remoteSlugs { - if !localSet[remoteSlug] { - functionsToDelete = append(functionsToDelete, remoteSlug) - } - } - - // If no functions to prune, return early - if len(functionsToDelete) == 0 { - fmt.Fprintln(os.Stderr, "No functions to prune.") - return nil - } - - // In interactive mode, prompt for confirmation (unless force is used) - if !force && utils.NewConsole().IsTTY { - message := fmt.Sprintf("The following functions will be DELETED from your project: %s", strings.Join(functionsToDelete, ", ")) - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), message) - - if confirmed, err := utils.NewConsole().PromptYesNo(ctx, "Are you sure?", false); err != nil { - return err - } else if !confirmed { - fmt.Fprintln(os.Stderr, "Aborted.") - return nil - } - } - - // Delete functions - for _, slug := range functionsToDelete { - fmt.Fprintf(os.Stderr, "Deleting function: %s\n", utils.Aqua(slug)) - resp, err := utils.GetSupabase().V1DeleteAFunctionWithResponse(ctx, projectRef, slug) - if err != nil { - return errors.Errorf("failed to delete function %s: %w", slug, err) - } - switch resp.StatusCode() { - case 404: - fmt.Fprintf(os.Stderr, "Function %s was already deleted.\n", utils.Aqua(slug)) - case 200: - fmt.Fprintf(os.Stderr, "Successfully deleted function: %s\n", utils.Aqua(slug)) - default: - return errors.Errorf("failed to delete function %s: %s", slug, string(resp.Body)) - } - } - - return nil -} - func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) (config.FunctionConfig, error) { // Although some functions do not require import map, it's more convenient to setup // vscode deno extension with a single import map for all functions. @@ -243,3 +161,50 @@ func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool, } return functionConfig, nil } + +// pruneFunctions deletes functions that exist remotely but not locally +func pruneFunctions(ctx context.Context, functionConfig config.FunctionConfig) error { + resp, err := utils.GetSupabase().V1ListAllFunctionsWithResponse(ctx, flags.ProjectRef) + if err != nil { + return errors.Errorf("failed to list functions: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) + } + var toDelete []string + for _, deployed := range *resp.JSON200 { + if deployed.Status == api.FunctionResponseStatusREMOVED { + continue + } else if _, exists := functionConfig[deployed.Slug]; exists { + // No need to delete disabled functions + continue + } + toDelete = append(toDelete, deployed.Slug) + } + if len(toDelete) == 0 { + fmt.Fprintln(os.Stderr, "No functions to prune.") + return nil + } + msg := fmt.Sprintln(confirmPruneAll(toDelete)) + if shouldDelete, err := utils.NewConsole().PromptYesNo(ctx, msg, false); err != nil { + return err + } else if !shouldDelete { + return errors.New(context.Canceled) + } + for _, slug := range toDelete { + fmt.Fprintln(os.Stderr, "Deleting Function:", slug) + if err := delete.Undeploy(ctx, flags.ProjectRef, slug); errors.Is(err, delete.ErrNoDelete) { + fmt.Fprintln(utils.GetDebugLogger(), err) + } else if err != nil { + return err + } + } + return nil +} + +func confirmPruneAll(pending []string) string { + msg := fmt.Sprintln("Do you want to delete the following functions?") + for _, slug := range pending { + msg += fmt.Sprintf(" • %s\n", utils.Bold(slug)) + } + return msg +} diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index 45bbe9b537..c5257b6388 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -11,6 +11,7 @@ import ( "github.com/h2non/gock" "github.com/spf13/afero" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" @@ -74,7 +75,7 @@ func TestDeployCommand(t *testing.T) { } // Run test noVerifyJWT := true - err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, false, false, fsys) + err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -129,7 +130,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) + err = Run(context.Background(), nil, true, nil, "", 1, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -182,7 +183,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, ".output_enabled-func") require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) + err = Run(context.Background(), nil, true, nil, "", 1, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -193,7 +194,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, false, false, fsys) + err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, false, fsys) // Check error assert.ErrorContains(t, err, "Invalid Function name.") }) @@ -203,7 +204,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) + err := Run(context.Background(), nil, true, nil, "", 1, false, fsys) // Check error assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions") }) @@ -249,7 +250,7 @@ verify_jwt = false outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, false, false, fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, false, fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -296,7 +297,7 @@ verify_jwt = false require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test noVerifyJWT := false - assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJWT, "", 1, false, false, fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJWT, "", 1, false, fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -375,6 +376,7 @@ func TestImportMapPath(t *testing.T) { func TestPruneFunctions(t *testing.T) { flags.ProjectRef = apitest.RandomProjectRef() + viper.Set("YES", true) t.Run("prunes functions not in local directory", func(t *testing.T) { // Setup in-memory fs @@ -422,7 +424,7 @@ func TestPruneFunctions(t *testing.T) { Reply(http.StatusOK) // Run test with prune and force (to skip confirmation) - err := Run(context.Background(), nil, false, nil, "", 1, true, true, fsys) + err := Run(context.Background(), nil, false, nil, "", 1, true, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -465,7 +467,7 @@ func TestPruneFunctions(t *testing.T) { JSON(api.BulkUpdateFunctionResponse{}) // Run test with prune and force - err := Run(context.Background(), nil, false, nil, "", 1, true, true, fsys) + err := Run(context.Background(), nil, false, nil, "", 1, true, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -510,7 +512,7 @@ func TestPruneFunctions(t *testing.T) { Reply(http.StatusNotFound) // Run test with prune and force - err := Run(context.Background(), nil, false, nil, "", 1, true, true, fsys) + err := Run(context.Background(), nil, false, nil, "", 1, true, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/internal/utils/console.go b/internal/utils/console.go index 85bebdc1ee..f80099aa34 100644 --- a/internal/utils/console.go +++ b/internal/utils/console.go @@ -10,6 +10,7 @@ import ( "time" "github.com/go-errors/errors" + "github.com/spf13/viper" "github.com/supabase/cli/pkg/cast" "golang.org/x/term" ) @@ -66,6 +67,10 @@ func (c *Console) PromptYesNo(ctx context.Context, label string, def bool) (bool choices = "y/N" } labelWithChoice := fmt.Sprintf("%s [%s] ", label, choices) + if viper.GetBool("YES") { + fmt.Fprintln(os.Stderr, labelWithChoice+"y") + return true, nil + } // Any error will be handled as default value input, err := c.PromptText(ctx, labelWithChoice) if len(input) > 0 { From 1b7cfa244a1288d8ddd553b60ebab19f6ceed11b Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Thu, 26 Jun 2025 00:23:24 +0800 Subject: [PATCH 3/4] chore: simplify unit tests --- cmd/functions.go | 2 +- internal/functions/deploy/deploy.go | 3 +- internal/functions/deploy/deploy_test.go | 102 +++++------------------ 3 files changed, 25 insertions(+), 82 deletions(-) diff --git a/cmd/functions.go b/cmd/functions.go index 9c542844a3..6b5684bc21 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -140,7 +140,7 @@ func init() { cobra.CheckErr(deployFlags.MarkHidden("legacy-bundle")) deployFlags.UintVarP(&maxJobs, "jobs", "j", 1, "Maximum number of parallel jobs.") deployFlags.BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") - deployFlags.BoolVar(&prune, "prune", false, "Delete Functions that exist in Supabase but not in local project.") + deployFlags.BoolVar(&prune, "prune", false, "Delete Functions that exist in Supabase project but not locally.") deployFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") deployFlags.StringVar(&importMapPath, "import-map", "", "Path to import map file.") functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 329678a16e..8c815e7795 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -170,12 +170,12 @@ func pruneFunctions(ctx context.Context, functionConfig config.FunctionConfig) e } else if resp.JSON200 == nil { return errors.Errorf("unexpected list functions status %d: %s", resp.StatusCode(), string(resp.Body)) } + // No need to delete disabled functions var toDelete []string for _, deployed := range *resp.JSON200 { if deployed.Status == api.FunctionResponseStatusREMOVED { continue } else if _, exists := functionConfig[deployed.Slug]; exists { - // No need to delete disabled functions continue } toDelete = append(toDelete, deployed.Slug) @@ -184,6 +184,7 @@ func pruneFunctions(ctx context.Context, functionConfig config.FunctionConfig) e fmt.Fprintln(os.Stderr, "No functions to prune.") return nil } + // Confirm before pruning functions msg := fmt.Sprintln(confirmPruneAll(toDelete)) if shouldDelete, err := utils.NewConsole().PromptYesNo(ctx, msg, false); err != nil { return err diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index c5257b6388..e746da9279 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -376,143 +376,85 @@ func TestImportMapPath(t *testing.T) { func TestPruneFunctions(t *testing.T) { flags.ProjectRef = apitest.RandomProjectRef() + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) viper.Set("YES", true) t.Run("prunes functions not in local directory", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) // Setup function entrypoints - localFunctions := []string{"local-func-1", "local-func-2"} - for _, fn := range localFunctions { - entrypointPath := filepath.Join(utils.FunctionsDir, fn, "index.ts") - require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + localFunctions := config.FunctionConfig{ + "local-func-1": {Enabled: true}, + "local-func-2": {Enabled: true}, } // Setup mock api - remote functions include local ones plus orphaned ones defer gock.OffAll() remoteFunctions := []api.FunctionResponse{ - {Slug: "local-func-1", Id: "1", Name: "local-func-1"}, - {Slug: "local-func-2", Id: "2", Name: "local-func-2"}, - {Slug: "orphaned-func-1", Id: "3", Name: "orphaned-func-1"}, - {Slug: "orphaned-func-2", Id: "4", Name: "orphaned-func-2"}, + {Slug: "local-func-1"}, + {Slug: "local-func-2"}, + {Slug: "orphaned-func-1"}, + {Slug: "orphaned-func-2"}, } gock.New(utils.DefaultApiHost). Get("/v1/projects/" + flags.ProjectRef + "/functions"). Reply(http.StatusOK). JSON(remoteFunctions) - // Mock deploy endpoint for local functions - for _, fn := range localFunctions { - gock.New(utils.DefaultApiHost). - Post("/v1/projects/"+flags.ProjectRef+"/functions"). - MatchParam("slug", fn). - Reply(http.StatusCreated). - JSON(api.FunctionResponse{Id: fn}) - } - gock.New(utils.DefaultApiHost). - Put("/v1/projects/" + flags.ProjectRef + "/functions"). - Reply(http.StatusOK). - JSON(api.BulkUpdateFunctionResponse{}) - // Mock delete endpoints for orphaned functions gock.New(utils.DefaultApiHost). Delete("/v1/projects/" + flags.ProjectRef + "/functions/orphaned-func-1"). Reply(http.StatusOK) gock.New(utils.DefaultApiHost). Delete("/v1/projects/" + flags.ProjectRef + "/functions/orphaned-func-2"). Reply(http.StatusOK) - // Run test with prune and force (to skip confirmation) - err := Run(context.Background(), nil, false, nil, "", 1, true, fsys) + err := pruneFunctions(context.Background(), localFunctions) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("skips pruning when no orphaned functions", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) // Setup function entrypoints - localFunctions := []string{"local-func-1", "local-func-2"} - for _, fn := range localFunctions { - entrypointPath := filepath.Join(utils.FunctionsDir, fn, "index.ts") - require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + localFunctions := config.FunctionConfig{ + "local-func-1": {}, + "local-func-2": {}, } // Setup mock api - remote functions match local ones exactly defer gock.OffAll() remoteFunctions := []api.FunctionResponse{ - {Slug: "local-func-1", Id: "1", Name: "local-func-1"}, - {Slug: "local-func-2", Id: "2", Name: "local-func-2"}, + {Slug: "local-func-1"}, + {Slug: "local-func-2"}, + {Slug: "orphaned-func-1", Status: api.FunctionResponseStatusREMOVED}, + {Slug: "orphaned-func-2", Status: api.FunctionResponseStatusREMOVED}, } gock.New(utils.DefaultApiHost). Get("/v1/projects/" + flags.ProjectRef + "/functions"). Reply(http.StatusOK). JSON(remoteFunctions) - // Mock deploy endpoint for local functions - for _, fn := range localFunctions { - gock.New(utils.DefaultApiHost). - Post("/v1/projects/"+flags.ProjectRef+"/functions"). - MatchParam("slug", fn). - Reply(http.StatusCreated). - JSON(api.FunctionResponse{Id: fn}) - } - gock.New(utils.DefaultApiHost). - Put("/v1/projects/" + flags.ProjectRef + "/functions"). - Reply(http.StatusOK). - JSON(api.BulkUpdateFunctionResponse{}) - // Run test with prune and force - err := Run(context.Background(), nil, false, nil, "", 1, true, fsys) + err := pruneFunctions(context.Background(), localFunctions) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("handles 404 on delete gracefully", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - require.NoError(t, utils.WriteConfig(fsys, false)) - // Setup valid access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) // Setup function entrypoints - localFunctions := []string{"local-func"} - for _, fn := range localFunctions { - entrypointPath := filepath.Join(utils.FunctionsDir, fn, "index.ts") - require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) - } + localFunctions := config.FunctionConfig{"local-func": {}} // Setup mock api defer gock.OffAll() remoteFunctions := []api.FunctionResponse{ - {Slug: "local-func", Id: "1", Name: "local-func"}, - {Slug: "orphaned-func", Id: "2", Name: "orphaned-func"}, + {Slug: "local-func"}, + {Slug: "orphaned-func"}, } gock.New(utils.DefaultApiHost). Get("/v1/projects/" + flags.ProjectRef + "/functions"). Reply(http.StatusOK). JSON(remoteFunctions) - // Mock deploy endpoint - gock.New(utils.DefaultApiHost). - Post("/v1/projects/"+flags.ProjectRef+"/functions"). - MatchParam("slug", "local-func"). - Reply(http.StatusCreated). - JSON(api.FunctionResponse{Id: "local-func"}) - gock.New(utils.DefaultApiHost). - Put("/v1/projects/" + flags.ProjectRef + "/functions"). - Reply(http.StatusOK). - JSON(api.BulkUpdateFunctionResponse{}) // Mock delete endpoint with 404 (function already deleted) gock.New(utils.DefaultApiHost). Delete("/v1/projects/" + flags.ProjectRef + "/functions/orphaned-func"). Reply(http.StatusNotFound) - // Run test with prune and force - err := Run(context.Background(), nil, false, nil, "", 1, true, fsys) + err := pruneFunctions(context.Background(), localFunctions) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) From 8f8f6a457d836775b6a8899ff33c8f6c26b777e4 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Thu, 26 Jun 2025 00:57:08 +0800 Subject: [PATCH 4/4] chore: update api spec --- api/overlay.yaml | 3 + pkg/api/types.gen.go | 221 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 220 insertions(+), 4 deletions(-) diff --git a/api/overlay.yaml b/api/overlay.yaml index 92b28afae9..b455dcc796 100644 --- a/api/overlay.yaml +++ b/api/overlay.yaml @@ -26,3 +26,6 @@ actions: - target: $.components.schemas.*.properties.connectionString description: Removes deprecated field that conflicts with naming convention remove: true +- target: $.components.schemas.*.properties.private_jwk.discriminator + description: Replaces discriminated union with concrete type + remove: true diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index c6cfdff106..1509530e37 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -211,6 +211,41 @@ const ( CreateSigningKeyBodyAlgorithmRS256 CreateSigningKeyBodyAlgorithm = "RS256" ) +// Defines values for CreateSigningKeyBodyPrivateJwk0E. +const ( + AQAB CreateSigningKeyBodyPrivateJwk0E = "AQAB" +) + +// Defines values for CreateSigningKeyBodyPrivateJwk0Kty. +const ( + RSA CreateSigningKeyBodyPrivateJwk0Kty = "RSA" +) + +// Defines values for CreateSigningKeyBodyPrivateJwk1Crv. +const ( + P256 CreateSigningKeyBodyPrivateJwk1Crv = "P-256" +) + +// Defines values for CreateSigningKeyBodyPrivateJwk1Kty. +const ( + EC CreateSigningKeyBodyPrivateJwk1Kty = "EC" +) + +// Defines values for CreateSigningKeyBodyPrivateJwk2Crv. +const ( + Ed25519 CreateSigningKeyBodyPrivateJwk2Crv = "Ed25519" +) + +// Defines values for CreateSigningKeyBodyPrivateJwk2Kty. +const ( + OKP CreateSigningKeyBodyPrivateJwk2Kty = "OKP" +) + +// Defines values for CreateSigningKeyBodyPrivateJwk3Kty. +const ( + Oct CreateSigningKeyBodyPrivateJwk3Kty = "oct" +) + // Defines values for CreateSigningKeyBodyStatus. const ( CreateSigningKeyBodyStatusInUse CreateSigningKeyBodyStatus = "in_use" @@ -1444,13 +1479,76 @@ type CreateSecretBody = []struct { // CreateSigningKeyBody defines model for CreateSigningKeyBody. type CreateSigningKeyBody struct { - Algorithm CreateSigningKeyBodyAlgorithm `json:"algorithm"` - Status *CreateSigningKeyBodyStatus `json:"status,omitempty"` + Algorithm CreateSigningKeyBodyAlgorithm `json:"algorithm"` + PrivateJwk *CreateSigningKeyBody_PrivateJwk `json:"private_jwk,omitempty"` + Status *CreateSigningKeyBodyStatus `json:"status,omitempty"` } // CreateSigningKeyBodyAlgorithm defines model for CreateSigningKeyBody.Algorithm. type CreateSigningKeyBodyAlgorithm string +// CreateSigningKeyBodyPrivateJwk0 defines model for . +type CreateSigningKeyBodyPrivateJwk0 struct { + D string `json:"d"` + Dp string `json:"dp"` + Dq string `json:"dq"` + E CreateSigningKeyBodyPrivateJwk0E `json:"e"` + Kty CreateSigningKeyBodyPrivateJwk0Kty `json:"kty"` + N string `json:"n"` + P string `json:"p"` + Q string `json:"q"` + Qi string `json:"qi"` +} + +// CreateSigningKeyBodyPrivateJwk0E defines model for CreateSigningKeyBody.PrivateJwk.0.E. +type CreateSigningKeyBodyPrivateJwk0E string + +// CreateSigningKeyBodyPrivateJwk0Kty defines model for CreateSigningKeyBody.PrivateJwk.0.Kty. +type CreateSigningKeyBodyPrivateJwk0Kty string + +// CreateSigningKeyBodyPrivateJwk1 defines model for . +type CreateSigningKeyBodyPrivateJwk1 struct { + Crv CreateSigningKeyBodyPrivateJwk1Crv `json:"crv"` + D string `json:"d"` + Kty CreateSigningKeyBodyPrivateJwk1Kty `json:"kty"` + X string `json:"x"` + Y string `json:"y"` +} + +// CreateSigningKeyBodyPrivateJwk1Crv defines model for CreateSigningKeyBody.PrivateJwk.1.Crv. +type CreateSigningKeyBodyPrivateJwk1Crv string + +// CreateSigningKeyBodyPrivateJwk1Kty defines model for CreateSigningKeyBody.PrivateJwk.1.Kty. +type CreateSigningKeyBodyPrivateJwk1Kty string + +// CreateSigningKeyBodyPrivateJwk2 defines model for . +type CreateSigningKeyBodyPrivateJwk2 struct { + Crv CreateSigningKeyBodyPrivateJwk2Crv `json:"crv"` + D string `json:"d"` + Kty CreateSigningKeyBodyPrivateJwk2Kty `json:"kty"` + X string `json:"x"` +} + +// CreateSigningKeyBodyPrivateJwk2Crv defines model for CreateSigningKeyBody.PrivateJwk.2.Crv. +type CreateSigningKeyBodyPrivateJwk2Crv string + +// CreateSigningKeyBodyPrivateJwk2Kty defines model for CreateSigningKeyBody.PrivateJwk.2.Kty. +type CreateSigningKeyBodyPrivateJwk2Kty string + +// CreateSigningKeyBodyPrivateJwk3 defines model for . +type CreateSigningKeyBodyPrivateJwk3 struct { + K string `json:"k"` + Kty CreateSigningKeyBodyPrivateJwk3Kty `json:"kty"` +} + +// CreateSigningKeyBodyPrivateJwk3Kty defines model for CreateSigningKeyBody.PrivateJwk.3.Kty. +type CreateSigningKeyBodyPrivateJwk3Kty string + +// CreateSigningKeyBody_PrivateJwk defines model for CreateSigningKeyBody.PrivateJwk. +type CreateSigningKeyBody_PrivateJwk struct { + union json.RawMessage +} + // CreateSigningKeyBodyStatus defines model for CreateSigningKeyBody.Status. type CreateSigningKeyBodyStatus string @@ -1954,15 +2052,16 @@ type ProjectUpgradeEligibilityResponse struct { CurrentAppVersionReleaseChannel ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel `json:"current_app_version_release_channel"` DurationEstimateHours float32 `json:"duration_estimate_hours"` Eligible bool `json:"eligible"` - ExtensionDependentObjects []string `json:"extension_dependent_objects"` LatestAppVersion string `json:"latest_app_version"` LegacyAuthCustomRoles []string `json:"legacy_auth_custom_roles"` - PotentialBreakingChanges []string `json:"potential_breaking_changes"` + ObjectsToBeDropped []string `json:"objects_to_be_dropped"` TargetUpgradeVersions []struct { AppVersion string `json:"app_version"` PostgresVersion ProjectUpgradeEligibilityResponseTargetUpgradeVersionsPostgresVersion `json:"postgres_version"` ReleaseChannel ProjectUpgradeEligibilityResponseTargetUpgradeVersionsReleaseChannel `json:"release_channel"` } `json:"target_upgrade_versions"` + UnsupportedExtensions []string `json:"unsupported_extensions"` + UserDefinedObjectsInInternalSchemas []string `json:"user_defined_objects_in_internal_schemas"` } // ProjectUpgradeEligibilityResponseCurrentAppVersionReleaseChannel defines model for ProjectUpgradeEligibilityResponse.CurrentAppVersionReleaseChannel. @@ -3552,6 +3651,120 @@ func (t *ApplyProjectAddonBody_AddonVariant) UnmarshalJSON(b []byte) error { return err } +// AsCreateSigningKeyBodyPrivateJwk0 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk0 +func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk0() (CreateSigningKeyBodyPrivateJwk0, error) { + var body CreateSigningKeyBodyPrivateJwk0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromCreateSigningKeyBodyPrivateJwk0 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk0 +func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk0(v CreateSigningKeyBodyPrivateJwk0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeCreateSigningKeyBodyPrivateJwk0 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk0 +func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk0(v CreateSigningKeyBodyPrivateJwk0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsCreateSigningKeyBodyPrivateJwk1 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk1 +func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk1() (CreateSigningKeyBodyPrivateJwk1, error) { + var body CreateSigningKeyBodyPrivateJwk1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromCreateSigningKeyBodyPrivateJwk1 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk1 +func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk1(v CreateSigningKeyBodyPrivateJwk1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeCreateSigningKeyBodyPrivateJwk1 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk1 +func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk1(v CreateSigningKeyBodyPrivateJwk1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsCreateSigningKeyBodyPrivateJwk2 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk2 +func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk2() (CreateSigningKeyBodyPrivateJwk2, error) { + var body CreateSigningKeyBodyPrivateJwk2 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromCreateSigningKeyBodyPrivateJwk2 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk2 +func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk2(v CreateSigningKeyBodyPrivateJwk2) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeCreateSigningKeyBodyPrivateJwk2 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk2 +func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk2(v CreateSigningKeyBodyPrivateJwk2) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsCreateSigningKeyBodyPrivateJwk3 returns the union data inside the CreateSigningKeyBody_PrivateJwk as a CreateSigningKeyBodyPrivateJwk3 +func (t CreateSigningKeyBody_PrivateJwk) AsCreateSigningKeyBodyPrivateJwk3() (CreateSigningKeyBodyPrivateJwk3, error) { + var body CreateSigningKeyBodyPrivateJwk3 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromCreateSigningKeyBodyPrivateJwk3 overwrites any union data inside the CreateSigningKeyBody_PrivateJwk as the provided CreateSigningKeyBodyPrivateJwk3 +func (t *CreateSigningKeyBody_PrivateJwk) FromCreateSigningKeyBodyPrivateJwk3(v CreateSigningKeyBodyPrivateJwk3) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeCreateSigningKeyBodyPrivateJwk3 performs a merge with any union data inside the CreateSigningKeyBody_PrivateJwk, using the provided CreateSigningKeyBodyPrivateJwk3 +func (t *CreateSigningKeyBody_PrivateJwk) MergeCreateSigningKeyBodyPrivateJwk3(v CreateSigningKeyBodyPrivateJwk3) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t CreateSigningKeyBody_PrivateJwk) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *CreateSigningKeyBody_PrivateJwk) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsListProjectAddonsResponseAvailableAddonsVariantsId0 returns the union data inside the ListProjectAddonsResponse_AvailableAddons_Variants_Id as a ListProjectAddonsResponseAvailableAddonsVariantsId0 func (t ListProjectAddonsResponse_AvailableAddons_Variants_Id) AsListProjectAddonsResponseAvailableAddonsVariantsId0() (ListProjectAddonsResponseAvailableAddonsVariantsId0, error) { var body ListProjectAddonsResponseAvailableAddonsVariantsId0