From 6bcbc1369e0755f2a63836a1d892892d318876c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Dvo=C5=99=C3=A1k?= <150935816+Dvorinka@users.noreply.github.com> Date: Sat, 10 Oct 2026 05:31:21 +0200 Subject: [PATCH] refactor(db): port git providers, repos, and webhook receiver to sqlc (#82) - git.sql: provider/repo/webhook CRUD, push-dispatch service matcher - ListServicesForPush folds the dynamic branch clause into a static predicate; COALESCE fallbacks match other service loaders - git_receiver dispatch writes reuse deployment queries - TestGitLiveInvariants: live coverage for upsert, search, webhook, and branch matching semantics Authored By: TDvorak --- app/backend/internal/api/git.go | 418 +++++----- app/backend/internal/api/git_receiver.go | 133 ++-- app/backend/internal/api/git_test.go | 204 ++++- .../internal/database/sqlcdb/git.sql.go | 737 ++++++++++++++++++ .../internal/database/sqlcdb/models.go | 40 + .../internal/database/sqlcdb/querier.go | 21 + app/backend/sqlc/queries/git.sql | 129 +++ app/backend/sqlc/schema.sql | 40 + 8 files changed, 1436 insertions(+), 286 deletions(-) create mode 100644 app/backend/internal/database/sqlcdb/git.sql.go create mode 100644 app/backend/sqlc/queries/git.sql diff --git a/app/backend/internal/api/git.go b/app/backend/internal/api/git.go index 0ac323a..5a7b8a9 100644 --- a/app/backend/internal/api/git.go +++ b/app/backend/internal/api/git.go @@ -2,6 +2,8 @@ package api import ( "containr/internal/database" + "containr/internal/database/sqlcdb" + "context" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -92,41 +94,73 @@ type CreateWebhookRequest struct { Branch string `json:"branch"` } +func gitRepoFrom(r sqlcdb.ListGitRepositoriesRow) GitRepository { + return GitRepository{ + ID: r.ID.String(), + ProviderID: r.ProviderID.String(), + Name: r.Name, + FullName: r.FullName, + Description: r.Description, + CloneURL: r.CloneUrl, + WebhookURL: r.WebhookUrl, + DefaultBranch: r.DefaultBranch.String, + IsPrivate: r.IsPrivate.Bool, + UserID: r.UserID.String(), + CreatedAt: r.CreatedAt.Time, + UpdatedAt: r.UpdatedAt.Time, + } +} + +func gitRepoFromFullNameRow(r sqlcdb.GetGitRepositoryByFullNameRow) GitRepository { + return GitRepository{ + ID: r.ID.String(), + ProviderID: r.ProviderID.String(), + Name: r.Name, + FullName: r.FullName, + Description: r.Description, + CloneURL: r.CloneUrl, + WebhookURL: r.WebhookUrl, + DefaultBranch: r.DefaultBranch.String, + IsPrivate: r.IsPrivate.Bool, + UserID: r.UserID.String(), + CreatedAt: r.CreatedAt.Time, + UpdatedAt: r.UpdatedAt.Time, + } +} + +func gitProviderFrom(p sqlcdb.ListGitProvidersRow) GitProvider { + return GitProvider{ + ID: p.ID.String(), + Name: p.Name, + DisplayName: p.DisplayName, + APIUrl: p.ApiUrl, + WebhookUrl: p.WebhookUrl, + UserID: p.UserID.String(), + CreatedAt: p.CreatedAt.Time, + UpdatedAt: p.UpdatedAt.Time, + } +} + func handleGetGitProviders(c *gin.Context) { ctx, ok := requireGitRequestContext(c) if !ok { return } - rows, err := ctx.db.Query(` - SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at - FROM git_providers - WHERE user_id = $1 - ORDER BY created_at DESC - `, ctx.userID) + userID, err := uuid.Parse(ctx.userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) + return + } + rows, err := sqlcdb.New(ctx.db.DB).ListGitProviders(context.Background(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } - defer rows.Close() - providers := make([]GitProvider, 0) - for rows.Next() { - var provider GitProvider - if err := rows.Scan( - &provider.ID, - &provider.Name, - &provider.DisplayName, - &provider.APIUrl, - &provider.WebhookUrl, - &provider.UserID, - &provider.CreatedAt, - &provider.UpdatedAt, - ); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) - return - } - providers = append(providers, provider) + providers := make([]GitProvider, 0, len(rows)) + for _, p := range rows { + providers = append(providers, gitProviderFrom(p)) } c.JSON(http.StatusOK, gin.H{"providers": providers}) @@ -173,21 +207,22 @@ func handleConnectGitHubApp(c *gin.Context) { UserID: ctx.userID, } - if err := ctx.db.QueryRow(` - INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) - ON CONFLICT (name, user_id) - DO UPDATE SET - display_name = EXCLUDED.display_name, - api_url = EXCLUDED.api_url, - webhook_url = EXCLUDED.webhook_url, - access_token = EXCLUDED.access_token, - updated_at = NOW() - RETURNING id, created_at, updated_at - `, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID).Scan(&provider.ID, &provider.CreatedAt, &provider.UpdatedAt); err != nil { + prow, err := sqlcdb.New(ctx.db.DB).UpsertGitHubAppProvider(context.Background(), sqlcdb.UpsertGitHubAppProviderParams{ + ID: uuid.MustParse(provider.ID), + Name: provider.Name, + DisplayName: provider.DisplayName, + ApiUrl: provider.APIUrl, + WebhookUrl: provider.WebhookUrl, + AccessToken: provider.AccessToken, + UserID: uuid.MustParse(provider.UserID), + }) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect GitHub App installation"}) return } + provider.ID = prow.ID.String() + provider.CreatedAt = prow.CreatedAt.Time + provider.UpdatedAt = prow.UpdatedAt.Time provider.AccessToken = "" c.JSON(http.StatusOK, gin.H{ @@ -259,10 +294,16 @@ func handleCreateGitProvider(c *gin.Context) { return } - if _, err := ctx.db.Exec(` - INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) - `, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl, provider.WebhookUrl, provider.AccessToken, provider.UserID); err != nil { + q := sqlcdb.New(ctx.db.DB) + if err := q.InsertGitProvider(context.Background(), sqlcdb.InsertGitProviderParams{ + ID: uuid.MustParse(provider.ID), + Name: provider.Name, + DisplayName: provider.DisplayName, + ApiUrl: provider.APIUrl, + WebhookUrl: provider.WebhookUrl, + AccessToken: provider.AccessToken, + UserID: uuid.MustParse(provider.UserID), + }); err != nil { if isUniqueConstraintError(err) { c.JSON(http.StatusConflict, gin.H{"error": "Provider already connected for this account"}) return @@ -271,13 +312,12 @@ func handleCreateGitProvider(c *gin.Context) { return } - if err := ctx.db.QueryRow(` - SELECT created_at, updated_at - FROM git_providers - WHERE id = $1 - `, provider.ID).Scan(&provider.CreatedAt, &provider.UpdatedAt); err != nil { + if ts, err := q.GetGitProviderTimestamps(context.Background(), uuid.MustParse(provider.ID)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read created provider"}) return + } else { + provider.CreatedAt = ts.CreatedAt.Time + provider.UpdatedAt = ts.UpdatedAt.Time } provider.AccessToken = "" @@ -296,12 +336,11 @@ func handleGetGitRepositories(c *gin.Context) { return } - var providerName, providerDisplayName, providerAccessToken, providerAPIURL string - err := ctx.db.QueryRow(` - SELECT name, display_name, access_token, api_url - FROM git_providers - WHERE id = $1 AND user_id = $2 - `, providerID, ctx.userID).Scan(&providerName, &providerDisplayName, &providerAccessToken, &providerAPIURL) + q := sqlcdb.New(ctx.db.DB) + provRow, err := q.GetGitProviderForRepos(context.Background(), sqlcdb.GetGitProviderForReposParams{ + ID: uuid.MustParse(providerID), + UserID: uuid.MustParse(ctx.userID), + }) if err != nil { if errors.Is(err, sql.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) @@ -310,6 +349,10 @@ func handleGetGitRepositories(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } + providerName := provRow.Name + providerDisplayName := provRow.DisplayName + providerAccessToken := provRow.AccessToken + providerAPIURL := provRow.ApiUrl if c.DefaultQuery("sync", "1") != "0" { if syncErr := syncProviderRepositories(ctx.db, providerID, ctx.userID, providerName, providerAPIURL, providerAccessToken); syncErr != nil { @@ -324,53 +367,30 @@ func handleGetGitRepositories(c *gin.Context) { } offset := (page - 1) * limit search := strings.TrimSpace(c.Query("search")) - searchNullable := sql.NullString{String: search, Valid: search != ""} - rows, err := ctx.db.Query(` - SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(webhook_url, ''), - default_branch, is_private, user_id, created_at, updated_at - FROM git_repositories - WHERE provider_id = $1 AND user_id = $2 - AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%')) - ORDER BY updated_at DESC - LIMIT $4 OFFSET $5 - `, providerID, ctx.userID, searchNullable, limit, offset) + repoRows, err := q.ListGitRepositories(context.Background(), sqlcdb.ListGitRepositoriesParams{ + ProviderID: uuid.MustParse(providerID), + UserID: uuid.MustParse(ctx.userID), + Column3: search, + Limit: int32(limit), + Offset: int32(offset), + }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } - defer rows.Close() - repositories := make([]GitRepository, 0) - for rows.Next() { - var repo GitRepository - if err := rows.Scan( - &repo.ID, - &repo.ProviderID, - &repo.Name, - &repo.FullName, - &repo.Description, - &repo.CloneURL, - &repo.WebhookURL, - &repo.DefaultBranch, - &repo.IsPrivate, - &repo.UserID, - &repo.CreatedAt, - &repo.UpdatedAt, - ); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) - return - } - repositories = append(repositories, repo) + repositories := make([]GitRepository, 0, len(repoRows)) + for _, r := range repoRows { + repositories = append(repositories, gitRepoFrom(r)) } - var total int - if err := ctx.db.QueryRow(` - SELECT COUNT(*) - FROM git_repositories - WHERE provider_id = $1 AND user_id = $2 - AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%')) - `, providerID, ctx.userID, searchNullable).Scan(&total); err != nil { + total, err := q.CountGitRepositories(context.Background(), sqlcdb.CountGitRepositoriesParams{ + ProviderID: uuid.MustParse(providerID), + UserID: uuid.MustParse(ctx.userID), + Column3: search, + }) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } @@ -408,12 +428,11 @@ func handleConnectGitRepository(c *gin.Context) { return } - var providerName string - err := ctx.db.QueryRow(` - SELECT name - FROM git_providers - WHERE id = $1 AND user_id = $2 - `, providerID, ctx.userID).Scan(&providerName) + q := sqlcdb.New(ctx.db.DB) + providerName, err := q.GetGitProviderName(context.Background(), sqlcdb.GetGitProviderNameParams{ + ID: uuid.MustParse(providerID), + UserID: uuid.MustParse(ctx.userID), + }) if err != nil { if errors.Is(err, sql.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) @@ -429,31 +448,16 @@ func handleConnectGitRepository(c *gin.Context) { return } - var existing GitRepository - err = ctx.db.QueryRow(` - SELECT id, provider_id, name, full_name, COALESCE(description, ''), clone_url, COALESCE(webhook_url, ''), - default_branch, is_private, user_id, created_at, updated_at - FROM git_repositories - WHERE provider_id = $1 AND full_name = $2 AND user_id = $3 - `, providerID, fullName, ctx.userID).Scan( - &existing.ID, - &existing.ProviderID, - &existing.Name, - &existing.FullName, - &existing.Description, - &existing.CloneURL, - &existing.WebhookURL, - &existing.DefaultBranch, - &existing.IsPrivate, - &existing.UserID, - &existing.CreatedAt, - &existing.UpdatedAt, - ) + existingRow, err := q.GetGitRepositoryByFullName(context.Background(), sqlcdb.GetGitRepositoryByFullNameParams{ + ProviderID: uuid.MustParse(providerID), + FullName: fullName, + UserID: uuid.MustParse(ctx.userID), + }) switch { case err == nil: c.JSON(http.StatusOK, gin.H{ "message": "Repository already connected", - "repository": existing, + "repository": gitRepoFromFullNameRow(existingRow), }) return case !errors.Is(err, sql.ErrNoRows): @@ -473,13 +477,18 @@ func handleConnectGitRepository(c *gin.Context) { UserID: ctx.userID, } - if err := ctx.db.QueryRow(` - INSERT INTO git_repositories ( - id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) - RETURNING created_at, updated_at - `, repo.ID, repo.ProviderID, repo.Name, repo.FullName, repo.Description, repo.CloneURL, repo.DefaultBranch, repo.IsPrivate, repo.UserID).Scan(&repo.CreatedAt, &repo.UpdatedAt); err != nil { + ins, err := q.InsertGitRepository(context.Background(), sqlcdb.InsertGitRepositoryParams{ + ID: uuid.MustParse(repo.ID), + ProviderID: uuid.MustParse(repo.ProviderID), + Name: repo.Name, + FullName: repo.FullName, + Description: sql.NullString{String: repo.Description, Valid: true}, + CloneUrl: repo.CloneURL, + DefaultBranch: sql.NullString{String: repo.DefaultBranch, Valid: true}, + IsPrivate: sql.NullBool{Bool: repo.IsPrivate, Valid: true}, + UserID: uuid.MustParse(repo.UserID), + }) + if err != nil { if isUniqueConstraintError(err) { c.JSON(http.StatusConflict, gin.H{"error": "Repository is already connected"}) return @@ -487,6 +496,8 @@ func handleConnectGitRepository(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"}) return } + repo.CreatedAt = ins.CreatedAt.Time + repo.UpdatedAt = ins.UpdatedAt.Time c.JSON(http.StatusCreated, gin.H{ "message": "Repository connected successfully", @@ -516,12 +527,10 @@ func handleCreateWebhook(c *gin.Context) { return } - var providerID string - err := ctx.db.QueryRow(` - SELECT provider_id - FROM git_repositories - WHERE id = $1 AND user_id = $2 - `, repoID, ctx.userID).Scan(&providerID) + providerUUID, err := sqlcdb.New(ctx.db.DB).GetGitRepositoryProviderID(context.Background(), sqlcdb.GetGitRepositoryProviderIDParams{ + ID: uuid.MustParse(repoID), + UserID: uuid.MustParse(ctx.userID), + }) if err != nil { if errors.Is(err, sql.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"}) @@ -530,6 +539,7 @@ func handleCreateWebhook(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } + providerID := providerUUID.String() eventsJSON, err := json.Marshal(req.Events) if err != nil { @@ -552,23 +562,22 @@ func handleCreateWebhook(c *gin.Context) { Active: true, } - if err := ctx.db.QueryRow(` - INSERT INTO git_webhooks ( - id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at - ) - VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW()) - ON CONFLICT (repo_id, provider_id) - DO UPDATE SET - events = EXCLUDED.events, - webhook_secret = EXCLUDED.webhook_secret, - active = TRUE, - branch_filter = EXCLUDED.branch_filter, - updated_at = NOW() - RETURNING id, active, created_at, updated_at - `, webhook.ID, webhook.RepoID, webhook.ProviderID, webhook.Events, webhook.Secret, strings.TrimSpace(req.Branch)).Scan(&webhook.ID, &webhook.Active, &webhook.CreatedAt, &webhook.UpdatedAt); err != nil { + wrow, err := sqlcdb.New(ctx.db.DB).UpsertGitWebhook(context.Background(), sqlcdb.UpsertGitWebhookParams{ + ID: uuid.MustParse(webhook.ID), + RepoID: uuid.MustParse(webhook.RepoID), + ProviderID: uuid.MustParse(webhook.ProviderID), + Events: webhook.Events, + WebhookSecret: webhook.Secret, + BranchFilter: sql.NullString{String: strings.TrimSpace(req.Branch), Valid: true}, + }) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to configure webhook"}) return } + webhook.ID = wrow.ID.String() + webhook.Active = wrow.Active.Bool + webhook.CreatedAt = wrow.CreatedAt.Time + webhook.UpdatedAt = wrow.UpdatedAt.Time c.JSON(http.StatusCreated, gin.H{ "message": "Webhook configured successfully", @@ -598,69 +607,39 @@ func handleGetConnectedRepositories(c *gin.Context) { } offset := (page - 1) * limit - rows, err := ctx.db.Query(` - SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, ''), r.clone_url, - r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at, - p.name as provider_name, p.display_name - FROM git_repositories r - JOIN git_providers p ON r.provider_id = p.id - WHERE r.user_id = $1 - ORDER BY r.updated_at DESC - LIMIT $2 OFFSET $3 - `, ctx.userID, limit, offset) + q := sqlcdb.New(ctx.db.DB) + repoRows, err := q.ListConnectedRepositories(context.Background(), sqlcdb.ListConnectedRepositoriesParams{ + UserID: uuid.MustParse(ctx.userID), + Limit: int32(limit), + Offset: int32(offset), + }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } - defer rows.Close() - - repositories := make([]map[string]interface{}, 0) - for rows.Next() { - var repo GitRepository - var providerName, providerDisplayName string - if err := rows.Scan( - &repo.ID, - &repo.ProviderID, - &repo.Name, - &repo.FullName, - &repo.Description, - &repo.CloneURL, - &repo.DefaultBranch, - &repo.IsPrivate, - &repo.UserID, - &repo.CreatedAt, - &repo.UpdatedAt, - &providerName, - &providerDisplayName, - ); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) - return - } + repositories := make([]map[string]interface{}, 0, len(repoRows)) + for _, r := range repoRows { repositories = append(repositories, map[string]interface{}{ - "id": repo.ID, - "provider_id": repo.ProviderID, - "name": repo.Name, - "full_name": repo.FullName, - "description": repo.Description, - "clone_url": repo.CloneURL, - "default_branch": repo.DefaultBranch, - "is_private": repo.IsPrivate, - "created_at": repo.CreatedAt, - "updated_at": repo.UpdatedAt, + "id": r.ID.String(), + "provider_id": r.ProviderID.String(), + "name": r.Name, + "full_name": r.FullName, + "description": r.Description, + "clone_url": r.CloneUrl, + "default_branch": r.DefaultBranch.String, + "is_private": r.IsPrivate.Bool, + "created_at": r.CreatedAt.Time, + "updated_at": r.UpdatedAt.Time, "provider": map[string]string{ - "name": providerName, - "display_name": providerDisplayName, + "name": r.ProviderName, + "display_name": r.DisplayName, }, }) } - var total int - if err := ctx.db.QueryRow(` - SELECT COUNT(*) - FROM git_repositories - WHERE user_id = $1 - `, ctx.userID).Scan(&total); err != nil { + total, err := q.CountUserRepositories(context.Background(), uuid.MustParse(ctx.userID)) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } @@ -705,20 +684,17 @@ func syncProviderRepositories(db *database.DB, providerID, userID, providerName, defaultBranch = "main" } - _, err := db.Exec(` - INSERT INTO git_repositories ( - id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) - ON CONFLICT (provider_id, full_name) - DO UPDATE SET - name = EXCLUDED.name, - description = EXCLUDED.description, - clone_url = EXCLUDED.clone_url, - default_branch = EXCLUDED.default_branch, - is_private = EXCLUDED.is_private, - updated_at = NOW() - `, uuid.NewString(), providerID, repository.Name, repository.FullName, repository.Description, cloneURL, defaultBranch, repository.IsPrivate, userID) + err := sqlcdb.New(db.DB).UpsertGitRepository(context.Background(), sqlcdb.UpsertGitRepositoryParams{ + ID: uuid.New(), + ProviderID: uuid.MustParse(providerID), + Name: repository.Name, + FullName: repository.FullName, + Description: sql.NullString{String: repository.Description, Valid: true}, + CloneUrl: cloneURL, + DefaultBranch: sql.NullString{String: defaultBranch, Valid: true}, + IsPrivate: sql.NullBool{Bool: repository.IsPrivate, Valid: true}, + UserID: uuid.MustParse(userID), + }) if err != nil { return err } @@ -1323,16 +1299,15 @@ func handleDeleteGitProvider(c *gin.Context) { return } - result, err := ctx.db.Exec( - "DELETE FROM git_providers WHERE id = $1 AND user_id = $2", - providerID, ctx.userID, - ) + affected, err := sqlcdb.New(ctx.db.DB).DeleteGitProvider(context.Background(), sqlcdb.DeleteGitProviderParams{ + ID: uuid.MustParse(providerID), + UserID: uuid.MustParse(ctx.userID), + }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"}) return } - affected, _ := result.RowsAffected() if affected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) return @@ -1362,12 +1337,10 @@ func handleGetGitRepositoryBranches(c *gin.Context) { return } - var providerName, providerAPIURL, providerAccessToken string - err := ctx.db.QueryRow(` - SELECT name, api_url, access_token - FROM git_providers - WHERE id = $1 AND user_id = $2 - `, providerID, ctx.userID).Scan(&providerName, &providerAPIURL, &providerAccessToken) + pf, err := sqlcdb.New(ctx.db.DB).GetGitProviderForFetch(context.Background(), sqlcdb.GetGitProviderForFetchParams{ + ID: uuid.MustParse(providerID), + UserID: uuid.MustParse(ctx.userID), + }) if err != nil { if errors.Is(err, sql.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) @@ -1376,6 +1349,9 @@ func handleGetGitRepositoryBranches(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } + providerName := pf.Name + providerAPIURL := pf.ApiUrl + providerAccessToken := pf.AccessToken branches, err := fetchRepositoryBranches(providerName, providerAPIURL, providerAccessToken, owner, repo) if err != nil { diff --git a/app/backend/internal/api/git_receiver.go b/app/backend/internal/api/git_receiver.go index fdcde23..3cc2fdc 100644 --- a/app/backend/internal/api/git_receiver.go +++ b/app/backend/internal/api/git_receiver.go @@ -5,6 +5,7 @@ import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" + "database/sql" "encoding/hex" "encoding/json" "fmt" @@ -14,6 +15,7 @@ import ( "time" "containr/internal/database" + "containr/internal/database/sqlcdb" "containr/internal/deployment" "containr/internal/deployqueue" @@ -38,22 +40,15 @@ func handleGitWebhookPush(c *gin.Context) { return } - var ( - secret string - repoID string - providerID string - branchFilter string - active bool - ) - err := db.QueryRow(` - SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, ''), active - FROM git_webhooks WHERE id = $1`, webhookID). - Scan(&secret, &repoID, &providerID, &branchFilter, &active) + q := sqlcdb.New(db.DB) + hook, err := q.GetGitWebhookForPush(context.Background(), uuid.MustParse(webhookID)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"}) return } - if !active { + secret := hook.WebhookSecret + branchFilter := hook.BranchFilter + if !hook.Active.Bool { c.JSON(http.StatusForbidden, gin.H{"error": "Webhook is disabled"}) return } @@ -64,8 +59,7 @@ func handleGitWebhookPush(c *gin.Context) { return } - var providerName string - _ = db.QueryRow(`SELECT name FROM git_providers WHERE id = $1`, providerID).Scan(&providerName) + providerName, _ := q.GetGitProviderNameByID(context.Background(), hook.ProviderID) if !verifyGitWebhookSignature(providerName, secret, c.Request, body) { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"}) @@ -118,14 +112,12 @@ func handleGitWebhookPush(c *gin.Context) { commit = payload.After } - var cloneURL, fullName, repoUserID string - err = db.QueryRow( - `SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1`, repoID, - ).Scan(&cloneURL, &fullName, &repoUserID) + repo, err := q.GetGitRepoForPush(context.Background(), hook.RepoID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"}) return } + cloneURL, fullName, repoUserID := repo.CloneUrl, repo.FullName, repo.UserID.String() engineValue, _ := c.Get("deployment_engine") engine, _ := engineValue.(*deployment.DeploymentEngine) @@ -147,34 +139,37 @@ func handleGitWebhookPush(c *gin.Context) { // the pushed repo+branch. Shared by the per-repo webhook receiver and the // GitHub App webhook endpoint. func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment.DeploymentEngine, cloneURL, fullName, branch, commit, repoUserID string) (int, error) { - // Sync-style events may carry no branch — match services on repo only. - branchClause := "s.git_branch = $1" - if branch == "" { - branchClause = "TRUE" - } - rows, err := db.Query(fmt.Sprintf(` - SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command, - s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory, - s.created_at, s.updated_at - FROM services s - WHERE %s AND (s.git_repo = $2 OR s.git_repo = $3)`, branchClause), - branch, cloneURL, fullName) + q := sqlcdb.New(db.DB) + ctx := context.Background() + // Sync-style events may carry no branch — empty branch matches all. + rows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{ + GitBranch: sql.NullString{String: branch, Valid: true}, + GitRepo: sql.NullString{String: cloneURL, Valid: true}, + GitRepo_2: sql.NullString{String: fullName, Valid: true}, + }) if err != nil { return 0, err } - defer rows.Close() var services []Service - for rows.Next() { - var s Service - if err := rows.Scan( - &s.ID, &s.ProjectID, &s.Name, &s.Type, &s.Status, &s.Image, &s.Command, - &s.Environment, &s.GitRepo, &s.GitBranch, &s.BuildPath, &s.CPU, &s.Memory, - &s.CreatedAt, &s.UpdatedAt, - ); err != nil { - return 0, err - } - services = append(services, s) + for _, row := range rows { + services = append(services, Service{ + ID: row.ID, + ProjectID: row.ProjectID, + Name: row.Name, + Type: row.Type, + Status: row.Status, + Image: row.Image, + Command: row.Command, + Environment: row.Environment, + GitRepo: row.GitRepo, + GitBranch: row.GitBranch, + BuildPath: row.BuildPath, + CPU: row.Cpu, + Memory: row.Memory, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + }) } enqueued := 0 @@ -192,27 +187,34 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment. CreatedAt: now, UpdatedAt: now, } - if _, err := db.Exec( - `INSERT INTO deployments - (id, service_id, version, commit_hash, status, image_name, image_tag, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - d.ID, d.ServiceID, fmt.Sprintf("v%d", now.Unix()), d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt, - ); err != nil { + if err := q.InsertDeployment(ctx, sqlcdb.InsertDeploymentParams{ + ID: d.ID, + ServiceID: d.ServiceID, + Version: fmt.Sprintf("v%d", now.Unix()), + CommitHash: sql.NullString{String: ptrStr(d.CommitHash), Valid: d.CommitHash != nil}, + Status: sql.NullString{String: d.Status, Valid: true}, + ImageName: sql.NullString{String: d.ImageName, Valid: true}, + ImageTag: sql.NullString{String: d.ImageTag, Valid: true}, + CreatedAt: sql.NullTime{Time: d.CreatedAt, Valid: true}, + UpdatedAt: sql.NullTime{Time: d.UpdatedAt, Valid: true}, + }); err != nil { continue } if engine == nil { - failedAt := time.Now() failure := "Deployment engine unavailable. Docker may not be configured on this server." - _, _ = db.Exec( - `UPDATE deployments SET status = 'failed', error = $1, completed_at = $2, updated_at = $2 WHERE id = $3`, - failure, failedAt, d.ID, - ) + failedAt := time.Now() + _ = q.FailDeployment(ctx, sqlcdb.FailDeploymentParams{ + Error: sql.NullString{String: failure, Valid: true}, + CompletedAt: sql.NullTime{Time: failedAt, Valid: true}, + ID: d.ID, + }) continue } - _, _ = db.Exec( - `UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`, - time.Now(), service.ID, - ) + _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ + Status: sql.NullString{String: "building", Valid: true}, + UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, + ID: service.ID, + }) // Webhook pushes default to a clean build — stale layers are the // classic silent-rollback failure (dflow convention). branchForBuild := branch @@ -226,14 +228,17 @@ func dispatchPushToServices(c *gin.Context, db *database.DB, engine *deployment. runDeploymentAndSync(jctx, db, engine, &d, service, webhookReq, repoUserID) }, }); pos > 0 { - _, _ = db.Exec( - `UPDATE deployments SET status = 'queued', updated_at = $1 WHERE id = $2`, - time.Now(), d.ID, - ) - _, _ = db.Exec( - `UPDATE services SET status = 'queued', updated_at = $1 WHERE id = $2`, - time.Now(), service.ID, - ) + now := sql.NullTime{Time: time.Now(), Valid: true} + _ = q.SetDeploymentStatus(ctx, sqlcdb.SetDeploymentStatusParams{ + Status: sql.NullString{String: "queued", Valid: true}, + UpdatedAt: now, + ID: d.ID, + }) + _ = q.SetServiceStatus(ctx, sqlcdb.SetServiceStatusParams{ + Status: sql.NullString{String: "queued", Valid: true}, + UpdatedAt: now, + ID: service.ID, + }) } enqueued++ } diff --git a/app/backend/internal/api/git_test.go b/app/backend/internal/api/git_test.go index 5ab2cc6..faac91b 100644 --- a/app/backend/internal/api/git_test.go +++ b/app/backend/internal/api/git_test.go @@ -1,6 +1,16 @@ package api -import "testing" +import ( + "context" + "database/sql" + "strings" + "testing" + + "containr/internal/database" + "containr/internal/database/sqlcdb" + + "github.com/google/uuid" +) func TestNormalizeRepositoryFullName(t *testing.T) { cases := []struct { @@ -83,3 +93,195 @@ func TestDeriveCloneURL(t *testing.T) { }) } } + +// Live invariant test against real Postgres — set METRICS_TEST_DSN (or +// DATABASE_URL). Covers the generated git provider/repo/webhook queries plus +// the push-dispatch service matcher (empty branch must match all branches). +func TestGitLiveInvariants(t *testing.T) { + dsn := testDSNString(t) + db, err := database.NewConnection(dsn) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer db.Close() + ctx := context.Background() + q := sqlcdb.New(db.DB) + + userID := uuid.New() + projectID := uuid.New() + envID := uuid.New() + providerID := uuid.New() + repoID := uuid.New() + webhookID := uuid.New() + svcMain := uuid.New() + svcDev := uuid.New() + suffix := strings.ReplaceAll(userID.String()[:8], "-", "") + must := func(err error) { + t.Helper() + if err != nil { + t.Fatalf("setup: %v", err) + } + } + _, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test')`, + userID, "git-"+suffix+"@test.local") + must(err) + _, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, true)`, + projectID, "git-"+suffix, userID) + must(err) + _, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`, + envID, projectID) + must(err) + defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id = $1`, userID) }() + + // Provider lifecycle. + must(q.InsertGitProvider(ctx, sqlcdb.InsertGitProviderParams{ + ID: providerID, Name: "github", DisplayName: "GitHub", + ApiUrl: "https://api.github.com", WebhookUrl: "https://hooks.test", + AccessToken: "tok", UserID: userID, + })) + providers, err := q.ListGitProviders(ctx, userID) + if err != nil || len(providers) == 0 { + t.Fatalf("providers: %v %v", err, providers) + } + name, err := q.GetGitProviderNameByID(ctx, providerID) + if err != nil || name != "github" { + t.Fatalf("provider name: %v %q", err, name) + } + fetch, err := q.GetGitProviderForFetch(ctx, sqlcdb.GetGitProviderForFetchParams{ID: providerID, UserID: userID}) + if err != nil || fetch.AccessToken != "tok" || fetch.ApiUrl != "https://api.github.com" { + t.Fatalf("fetch: %v %+v", err, fetch) + } + + // Repository insert + search. + cloneURL := "https://github.com/acme/svc-" + suffix + ".git" + fullName := "acme/svc-" + suffix + _, err = q.InsertGitRepository(ctx, sqlcdb.InsertGitRepositoryParams{ + ID: repoID, ProviderID: providerID, Name: "svc-" + suffix, FullName: fullName, + Description: sql.NullString{String: "", Valid: true}, + CloneUrl: cloneURL, + DefaultBranch: sql.NullString{String: "main", Valid: true}, + IsPrivate: sql.NullBool{Bool: false, Valid: true}, + UserID: userID, + }) + must(err) + byName, err := q.GetGitRepositoryByFullName(ctx, sqlcdb.GetGitRepositoryByFullNameParams{ + ProviderID: providerID, FullName: fullName, UserID: userID, + }) + if err != nil || byName.ID != repoID || byName.CloneUrl != cloneURL { + t.Fatalf("repo by full name: %v %+v", err, byName) + } + all, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{ + ProviderID: providerID, UserID: userID, Column3: "", Limit: 50, Offset: 0, + }) + if err != nil || len(all) == 0 { + t.Fatalf("repo list (empty search): %v %v", err, all) + } + hit, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{ + ProviderID: providerID, UserID: userID, Column3: "svc-" + suffix, Limit: 50, Offset: 0, + }) + if err != nil || len(hit) != 1 || hit[0].ID != repoID { + t.Fatalf("repo list (matching search): %v %v", err, hit) + } + miss, err := q.ListGitRepositories(ctx, sqlcdb.ListGitRepositoriesParams{ + ProviderID: providerID, UserID: userID, Column3: "zzz-no-match", Limit: 50, Offset: 0, + }) + if err != nil || len(miss) != 0 { + t.Fatalf("repo list (non-matching search): %v %v", err, miss) + } + count, err := q.CountGitRepositories(ctx, sqlcdb.CountGitRepositoriesParams{ + ProviderID: providerID, UserID: userID, Column3: "", + }) + if err != nil || count < 1 { + t.Fatalf("repo count: %v %d", err, count) + } + // Upsert on (provider_id, full_name) conflict — updates rather than errors. + must(q.UpsertGitRepository(ctx, sqlcdb.UpsertGitRepositoryParams{ + ID: uuid.New(), ProviderID: providerID, Name: "renamed-" + suffix, FullName: fullName, + Description: sql.NullString{String: "d", Valid: true}, + CloneUrl: cloneURL, + DefaultBranch: sql.NullString{String: "main", Valid: true}, + IsPrivate: sql.NullBool{Bool: true, Valid: true}, + UserID: userID, + })) + byName, err = q.GetGitRepositoryByFullName(ctx, sqlcdb.GetGitRepositoryByFullNameParams{ + ProviderID: providerID, FullName: fullName, UserID: userID, + }) + if err != nil || byName.Name != "renamed-"+suffix { + t.Fatalf("repo after upsert: %v %+v", err, byName) + } + + // Connected-repos join + webhook lifecycle. + connected, err := q.ListConnectedRepositories(ctx, sqlcdb.ListConnectedRepositoriesParams{ + UserID: userID, Limit: 50, Offset: 0, + }) + if err != nil || len(connected) == 0 { + t.Fatalf("connected repos: %v %v", err, connected) + } + wh, err := q.UpsertGitWebhook(ctx, sqlcdb.UpsertGitWebhookParams{ + ID: webhookID, RepoID: repoID, ProviderID: providerID, + Events: "push", WebhookSecret: "sec-" + suffix, + BranchFilter: sql.NullString{String: "", Valid: true}, + }) + must(err) + if !wh.Active.Bool { + t.Fatalf("webhook inactive after upsert: %+v", wh) + } + push, err := q.GetGitWebhookForPush(ctx, webhookID) + if err != nil || push.WebhookSecret != "sec-"+suffix || push.ProviderID != providerID { + t.Fatalf("webhook for push: %v %+v", err, push) + } + pushRepo, err := q.GetGitRepoForPush(ctx, repoID) + if err != nil || pushRepo.CloneUrl != cloneURL || pushRepo.FullName != fullName || pushRepo.UserID != userID { + t.Fatalf("repo for push: %v %+v", err, pushRepo) + } + + // Push-dispatch service matcher: branch filter and empty-branch match-all. + for id, branch := range map[uuid.UUID]string{svcMain: "main", svcDev: "dev"} { + _, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type, git_repo, git_branch) + VALUES ($1, $2, $3, $4, 'web', 'git', $5, $6)`, + id, "svc-"+suffix+"-"+branch, projectID, envID, cloneURL, branch) + must(err) + } + servicesFor := func(branch string) []sqlcdb.ListServicesForPushRow { + rows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{ + GitBranch: sql.NullString{String: branch, Valid: true}, + GitRepo: sql.NullString{String: cloneURL, Valid: true}, + GitRepo_2: sql.NullString{String: fullName, Valid: true}, + }) + if err != nil { + t.Fatalf("services for push %q: %v", branch, err) + } + return rows + } + mainRows := servicesFor("main") + if len(mainRows) != 1 || mainRows[0].ID != svcMain || mainRows[0].GitBranch != "main" { + t.Fatalf("main branch dispatch: %+v", mainRows) + } + devRows := servicesFor("dev") + if len(devRows) != 1 || devRows[0].ID != svcDev { + t.Fatalf("dev branch dispatch: %+v", devRows) + } + allRows := servicesFor("") + if len(allRows) != 2 { + t.Fatalf("empty-branch dispatch should match all: %+v", allRows) + } + // Repo may also be referenced by its full_name. + fullRows, err := q.ListServicesForPush(ctx, sqlcdb.ListServicesForPushParams{ + GitBranch: sql.NullString{String: "main", Valid: true}, + GitRepo: sql.NullString{String: "git@github.com:other/none.git", Valid: true}, + GitRepo_2: sql.NullString{String: cloneURL, Valid: true}, + }) + if err != nil || len(fullRows) != 1 { + t.Fatalf("full-name repo match: %v %+v", err, fullRows) + } + + // Provider delete is owner-scoped. + other, err := q.DeleteGitProvider(ctx, sqlcdb.DeleteGitProviderParams{ID: providerID, UserID: uuid.New()}) + if err != nil || other != 0 { + t.Fatalf("foreign delete affected %d rows: %v", other, err) + } + deleted, err := q.DeleteGitProvider(ctx, sqlcdb.DeleteGitProviderParams{ID: providerID, UserID: userID}) + if err != nil || deleted != 1 { + t.Fatalf("owner delete affected %d rows: %v", deleted, err) + } +} diff --git a/app/backend/internal/database/sqlcdb/git.sql.go b/app/backend/internal/database/sqlcdb/git.sql.go new file mode 100644 index 0000000..438974b --- /dev/null +++ b/app/backend/internal/database/sqlcdb/git.sql.go @@ -0,0 +1,737 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: git.sql + +package sqlcdb + +import ( + "context" + "database/sql" + + "github.com/google/uuid" +) + +const countGitRepositories = `-- name: CountGitRepositories :one +SELECT COUNT(*) +FROM git_repositories +WHERE provider_id = $1 AND user_id = $2 + AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%')) +` + +type CountGitRepositoriesParams struct { + ProviderID uuid.UUID `json:"provider_id"` + UserID uuid.UUID `json:"user_id"` + Column3 string `json:"column_3"` +} + +func (q *Queries) CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (int64, error) { + row := q.db.QueryRowContext(ctx, countGitRepositories, arg.ProviderID, arg.UserID, arg.Column3) + var count int64 + err := row.Scan(&count) + return count, err +} + +const countUserRepositories = `-- name: CountUserRepositories :one +SELECT COUNT(*) FROM git_repositories WHERE user_id = $1 +` + +func (q *Queries) CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error) { + row := q.db.QueryRowContext(ctx, countUserRepositories, userID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const deleteGitProvider = `-- name: DeleteGitProvider :execrows +DELETE FROM git_providers WHERE id = $1 AND user_id = $2 +` + +type DeleteGitProviderParams struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error) { + result, err := q.db.ExecContext(ctx, deleteGitProvider, arg.ID, arg.UserID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const getGitProviderForFetch = `-- name: GetGitProviderForFetch :one +SELECT name, api_url, access_token +FROM git_providers +WHERE id = $1 AND user_id = $2 +` + +type GetGitProviderForFetchParams struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` +} + +type GetGitProviderForFetchRow struct { + Name string `json:"name"` + ApiUrl string `json:"api_url"` + AccessToken string `json:"access_token"` +} + +func (q *Queries) GetGitProviderForFetch(ctx context.Context, arg GetGitProviderForFetchParams) (GetGitProviderForFetchRow, error) { + row := q.db.QueryRowContext(ctx, getGitProviderForFetch, arg.ID, arg.UserID) + var i GetGitProviderForFetchRow + err := row.Scan(&i.Name, &i.ApiUrl, &i.AccessToken) + return i, err +} + +const getGitProviderForRepos = `-- name: GetGitProviderForRepos :one +SELECT name, display_name, access_token, api_url +FROM git_providers +WHERE id = $1 AND user_id = $2 +` + +type GetGitProviderForReposParams struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` +} + +type GetGitProviderForReposRow struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + AccessToken string `json:"access_token"` + ApiUrl string `json:"api_url"` +} + +func (q *Queries) GetGitProviderForRepos(ctx context.Context, arg GetGitProviderForReposParams) (GetGitProviderForReposRow, error) { + row := q.db.QueryRowContext(ctx, getGitProviderForRepos, arg.ID, arg.UserID) + var i GetGitProviderForReposRow + err := row.Scan( + &i.Name, + &i.DisplayName, + &i.AccessToken, + &i.ApiUrl, + ) + return i, err +} + +const getGitProviderName = `-- name: GetGitProviderName :one +SELECT name FROM git_providers WHERE id = $1 AND user_id = $2 +` + +type GetGitProviderNameParams struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) GetGitProviderName(ctx context.Context, arg GetGitProviderNameParams) (string, error) { + row := q.db.QueryRowContext(ctx, getGitProviderName, arg.ID, arg.UserID) + var name string + err := row.Scan(&name) + return name, err +} + +const getGitProviderNameByID = `-- name: GetGitProviderNameByID :one +SELECT name FROM git_providers WHERE id = $1 +` + +func (q *Queries) GetGitProviderNameByID(ctx context.Context, id uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getGitProviderNameByID, id) + var name string + err := row.Scan(&name) + return name, err +} + +const getGitProviderTimestamps = `-- name: GetGitProviderTimestamps :one +SELECT created_at, updated_at FROM git_providers WHERE id = $1 +` + +type GetGitProviderTimestampsRow struct { + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (GetGitProviderTimestampsRow, error) { + row := q.db.QueryRowContext(ctx, getGitProviderTimestamps, id) + var i GetGitProviderTimestampsRow + err := row.Scan(&i.CreatedAt, &i.UpdatedAt) + return i, err +} + +const getGitRepoForPush = `-- name: GetGitRepoForPush :one +SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1 +` + +type GetGitRepoForPushRow struct { + CloneUrl string `json:"clone_url"` + FullName string `json:"full_name"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error) { + row := q.db.QueryRowContext(ctx, getGitRepoForPush, id) + var i GetGitRepoForPushRow + err := row.Scan(&i.CloneUrl, &i.FullName, &i.UserID) + return i, err +} + +const getGitRepositoryByFullName = `-- name: GetGitRepositoryByFullName :one +SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description, + clone_url, COALESCE(webhook_url, '') AS webhook_url, + default_branch, is_private, user_id, created_at, updated_at +FROM git_repositories +WHERE provider_id = $1 AND full_name = $2 AND user_id = $3 +` + +type GetGitRepositoryByFullNameParams struct { + ProviderID uuid.UUID `json:"provider_id"` + FullName string `json:"full_name"` + UserID uuid.UUID `json:"user_id"` +} + +type GetGitRepositoryByFullNameRow struct { + ID uuid.UUID `json:"id"` + ProviderID uuid.UUID `json:"provider_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + CloneUrl string `json:"clone_url"` + WebhookUrl string `json:"webhook_url"` + DefaultBranch sql.NullString `json:"default_branch"` + IsPrivate sql.NullBool `json:"is_private"` + UserID uuid.UUID `json:"user_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error) { + row := q.db.QueryRowContext(ctx, getGitRepositoryByFullName, arg.ProviderID, arg.FullName, arg.UserID) + var i GetGitRepositoryByFullNameRow + err := row.Scan( + &i.ID, + &i.ProviderID, + &i.Name, + &i.FullName, + &i.Description, + &i.CloneUrl, + &i.WebhookUrl, + &i.DefaultBranch, + &i.IsPrivate, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getGitRepositoryProviderID = `-- name: GetGitRepositoryProviderID :one +SELECT provider_id FROM git_repositories WHERE id = $1 AND user_id = $2 +` + +type GetGitRepositoryProviderIDParams struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error) { + row := q.db.QueryRowContext(ctx, getGitRepositoryProviderID, arg.ID, arg.UserID) + var provider_id uuid.UUID + err := row.Scan(&provider_id) + return provider_id, err +} + +const getGitWebhookForPush = `-- name: GetGitWebhookForPush :one +SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, '') AS branch_filter, active +FROM git_webhooks WHERE id = $1 +` + +type GetGitWebhookForPushRow struct { + WebhookSecret string `json:"webhook_secret"` + RepoID uuid.UUID `json:"repo_id"` + ProviderID uuid.UUID `json:"provider_id"` + BranchFilter string `json:"branch_filter"` + Active sql.NullBool `json:"active"` +} + +func (q *Queries) GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error) { + row := q.db.QueryRowContext(ctx, getGitWebhookForPush, id) + var i GetGitWebhookForPushRow + err := row.Scan( + &i.WebhookSecret, + &i.RepoID, + &i.ProviderID, + &i.BranchFilter, + &i.Active, + ) + return i, err +} + +const insertGitProvider = `-- name: InsertGitProvider :exec +INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) +` + +type InsertGitProviderParams struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + ApiUrl string `json:"api_url"` + WebhookUrl string `json:"webhook_url"` + AccessToken string `json:"access_token"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error { + _, err := q.db.ExecContext(ctx, insertGitProvider, + arg.ID, + arg.Name, + arg.DisplayName, + arg.ApiUrl, + arg.WebhookUrl, + arg.AccessToken, + arg.UserID, + ) + return err +} + +const insertGitRepository = `-- name: InsertGitRepository :one +INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) +RETURNING created_at, updated_at +` + +type InsertGitRepositoryParams struct { + ID uuid.UUID `json:"id"` + ProviderID uuid.UUID `json:"provider_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description sql.NullString `json:"description"` + CloneUrl string `json:"clone_url"` + DefaultBranch sql.NullString `json:"default_branch"` + IsPrivate sql.NullBool `json:"is_private"` + UserID uuid.UUID `json:"user_id"` +} + +type InsertGitRepositoryRow struct { + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error) { + row := q.db.QueryRowContext(ctx, insertGitRepository, + arg.ID, + arg.ProviderID, + arg.Name, + arg.FullName, + arg.Description, + arg.CloneUrl, + arg.DefaultBranch, + arg.IsPrivate, + arg.UserID, + ) + var i InsertGitRepositoryRow + err := row.Scan(&i.CreatedAt, &i.UpdatedAt) + return i, err +} + +const listConnectedRepositories = `-- name: ListConnectedRepositories :many +SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, '') AS description, r.clone_url, + r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at, + p.name AS provider_name, p.display_name +FROM git_repositories r +JOIN git_providers p ON r.provider_id = p.id +WHERE r.user_id = $1 +ORDER BY r.updated_at DESC +LIMIT $2 OFFSET $3 +` + +type ListConnectedRepositoriesParams struct { + UserID uuid.UUID `json:"user_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type ListConnectedRepositoriesRow struct { + ID uuid.UUID `json:"id"` + ProviderID uuid.UUID `json:"provider_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + CloneUrl string `json:"clone_url"` + DefaultBranch sql.NullString `json:"default_branch"` + IsPrivate sql.NullBool `json:"is_private"` + UserID uuid.UUID `json:"user_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` + ProviderName string `json:"provider_name"` + DisplayName string `json:"display_name"` +} + +func (q *Queries) ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error) { + rows, err := q.db.QueryContext(ctx, listConnectedRepositories, arg.UserID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListConnectedRepositoriesRow{} + for rows.Next() { + var i ListConnectedRepositoriesRow + if err := rows.Scan( + &i.ID, + &i.ProviderID, + &i.Name, + &i.FullName, + &i.Description, + &i.CloneUrl, + &i.DefaultBranch, + &i.IsPrivate, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + &i.ProviderName, + &i.DisplayName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listGitProviders = `-- name: ListGitProviders :many +SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at +FROM git_providers +WHERE user_id = $1 +ORDER BY created_at DESC +` + +type ListGitProvidersRow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + ApiUrl string `json:"api_url"` + WebhookUrl string `json:"webhook_url"` + UserID uuid.UUID `json:"user_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error) { + rows, err := q.db.QueryContext(ctx, listGitProviders, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListGitProvidersRow{} + for rows.Next() { + var i ListGitProvidersRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.DisplayName, + &i.ApiUrl, + &i.WebhookUrl, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listGitRepositories = `-- name: ListGitRepositories :many +SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description, + clone_url, COALESCE(webhook_url, '') AS webhook_url, + default_branch, is_private, user_id, created_at, updated_at +FROM git_repositories +WHERE provider_id = $1 AND user_id = $2 + AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%')) +ORDER BY updated_at DESC +LIMIT $4 OFFSET $5 +` + +type ListGitRepositoriesParams struct { + ProviderID uuid.UUID `json:"provider_id"` + UserID uuid.UUID `json:"user_id"` + Column3 string `json:"column_3"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type ListGitRepositoriesRow struct { + ID uuid.UUID `json:"id"` + ProviderID uuid.UUID `json:"provider_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + CloneUrl string `json:"clone_url"` + WebhookUrl string `json:"webhook_url"` + DefaultBranch sql.NullString `json:"default_branch"` + IsPrivate sql.NullBool `json:"is_private"` + UserID uuid.UUID `json:"user_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error) { + rows, err := q.db.QueryContext(ctx, listGitRepositories, + arg.ProviderID, + arg.UserID, + arg.Column3, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListGitRepositoriesRow{} + for rows.Next() { + var i ListGitRepositoriesRow + if err := rows.Scan( + &i.ID, + &i.ProviderID, + &i.Name, + &i.FullName, + &i.Description, + &i.CloneUrl, + &i.WebhookUrl, + &i.DefaultBranch, + &i.IsPrivate, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listServicesForPush = `-- name: ListServicesForPush :many +SELECT s.id, s.project_id, s.name, + COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status, + COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command, + COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo, + COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path, + COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory, + s.created_at, s.updated_at +FROM services s +WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3) +` + +type ListServicesForPushParams struct { + GitBranch sql.NullString `json:"git_branch"` + GitRepo sql.NullString `json:"git_repo"` + GitRepo_2 sql.NullString `json:"git_repo_2"` +} + +type ListServicesForPushRow struct { + ID uuid.UUID `json:"id"` + ProjectID uuid.UUID `json:"project_id"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` + Image string `json:"image"` + Command string `json:"command"` + Environment string `json:"environment"` + GitRepo string `json:"git_repo"` + GitBranch string `json:"git_branch"` + BuildPath string `json:"build_path"` + Cpu string `json:"cpu"` + Memory string `json:"memory"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error) { + rows, err := q.db.QueryContext(ctx, listServicesForPush, arg.GitBranch, arg.GitRepo, arg.GitRepo_2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListServicesForPushRow{} + for rows.Next() { + var i ListServicesForPushRow + if err := rows.Scan( + &i.ID, + &i.ProjectID, + &i.Name, + &i.Type, + &i.Status, + &i.Image, + &i.Command, + &i.Environment, + &i.GitRepo, + &i.GitBranch, + &i.BuildPath, + &i.Cpu, + &i.Memory, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const upsertGitHubAppProvider = `-- name: UpsertGitHubAppProvider :one +INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) +ON CONFLICT (name, user_id) +DO UPDATE SET + display_name = EXCLUDED.display_name, + api_url = EXCLUDED.api_url, + webhook_url = EXCLUDED.webhook_url, + access_token = EXCLUDED.access_token, + updated_at = NOW() +RETURNING id, created_at, updated_at +` + +type UpsertGitHubAppProviderParams struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + ApiUrl string `json:"api_url"` + WebhookUrl string `json:"webhook_url"` + AccessToken string `json:"access_token"` + UserID uuid.UUID `json:"user_id"` +} + +type UpsertGitHubAppProviderRow struct { + ID uuid.UUID `json:"id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error) { + row := q.db.QueryRowContext(ctx, upsertGitHubAppProvider, + arg.ID, + arg.Name, + arg.DisplayName, + arg.ApiUrl, + arg.WebhookUrl, + arg.AccessToken, + arg.UserID, + ) + var i UpsertGitHubAppProviderRow + err := row.Scan(&i.ID, &i.CreatedAt, &i.UpdatedAt) + return i, err +} + +const upsertGitRepository = `-- name: UpsertGitRepository :exec +INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) +ON CONFLICT (provider_id, full_name) +DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + clone_url = EXCLUDED.clone_url, + default_branch = EXCLUDED.default_branch, + is_private = EXCLUDED.is_private, + updated_at = NOW() +` + +type UpsertGitRepositoryParams struct { + ID uuid.UUID `json:"id"` + ProviderID uuid.UUID `json:"provider_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description sql.NullString `json:"description"` + CloneUrl string `json:"clone_url"` + DefaultBranch sql.NullString `json:"default_branch"` + IsPrivate sql.NullBool `json:"is_private"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error { + _, err := q.db.ExecContext(ctx, upsertGitRepository, + arg.ID, + arg.ProviderID, + arg.Name, + arg.FullName, + arg.Description, + arg.CloneUrl, + arg.DefaultBranch, + arg.IsPrivate, + arg.UserID, + ) + return err +} + +const upsertGitWebhook = `-- name: UpsertGitWebhook :one +INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW()) +ON CONFLICT (repo_id, provider_id) +DO UPDATE SET + events = EXCLUDED.events, + webhook_secret = EXCLUDED.webhook_secret, + active = TRUE, + branch_filter = EXCLUDED.branch_filter, + updated_at = NOW() +RETURNING id, active, created_at, updated_at +` + +type UpsertGitWebhookParams struct { + ID uuid.UUID `json:"id"` + RepoID uuid.UUID `json:"repo_id"` + ProviderID uuid.UUID `json:"provider_id"` + Events string `json:"events"` + WebhookSecret string `json:"webhook_secret"` + BranchFilter sql.NullString `json:"branch_filter"` +} + +type UpsertGitWebhookRow struct { + ID uuid.UUID `json:"id"` + Active sql.NullBool `json:"active"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +func (q *Queries) UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error) { + row := q.db.QueryRowContext(ctx, upsertGitWebhook, + arg.ID, + arg.RepoID, + arg.ProviderID, + arg.Events, + arg.WebhookSecret, + arg.BranchFilter, + ) + var i UpsertGitWebhookRow + err := row.Scan( + &i.ID, + &i.Active, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/app/backend/internal/database/sqlcdb/models.go b/app/backend/internal/database/sqlcdb/models.go index f40989b..4d4c0f6 100644 --- a/app/backend/internal/database/sqlcdb/models.go +++ b/app/backend/internal/database/sqlcdb/models.go @@ -200,6 +200,46 @@ type EnvironmentVariable struct { UpdatedAt sql.NullTime `json:"updated_at"` } +type GitProvider struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + ApiUrl string `json:"api_url"` + WebhookUrl string `json:"webhook_url"` + AccessToken string `json:"access_token"` + UserID uuid.UUID `json:"user_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type GitRepository struct { + ID uuid.UUID `json:"id"` + ProviderID uuid.UUID `json:"provider_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description sql.NullString `json:"description"` + CloneUrl string `json:"clone_url"` + WebhookUrl sql.NullString `json:"webhook_url"` + DefaultBranch sql.NullString `json:"default_branch"` + IsPrivate sql.NullBool `json:"is_private"` + UserID uuid.UUID `json:"user_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + +type GitWebhook struct { + ID uuid.UUID `json:"id"` + RepoID uuid.UUID `json:"repo_id"` + ProviderID uuid.UUID `json:"provider_id"` + Events string `json:"events"` + WebhookSecret string `json:"webhook_secret"` + RemoteWebhookID sql.NullString `json:"remote_webhook_id"` + Active sql.NullBool `json:"active"` + BranchFilter sql.NullString `json:"branch_filter"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` +} + type InstanceMetric struct { ServiceID string `json:"service_id"` Timestamp time.Time `json:"timestamp"` diff --git a/app/backend/internal/database/sqlcdb/querier.go b/app/backend/internal/database/sqlcdb/querier.go index d1578ad..988ad2b 100644 --- a/app/backend/internal/database/sqlcdb/querier.go +++ b/app/backend/internal/database/sqlcdb/querier.go @@ -29,11 +29,13 @@ type Querier interface { CountDatabasesUsingBackupTarget(ctx context.Context, targetID sql.NullString) (int64, error) CountDeploymentsForService(ctx context.Context, serviceID uuid.UUID) (int64, error) CountEnvironmentServices(ctx context.Context, environmentID uuid.UUID) (int64, error) + CountGitRepositories(ctx context.Context, arg CountGitRepositoriesParams) (int64, error) CountProjectsByUser(ctx context.Context, arg CountProjectsByUserParams) (int64, error) CountServiceDomains(ctx context.Context, serviceID uuid.UUID) (int64, error) CountServicesByName(ctx context.Context, arg CountServicesByNameParams) (int64, error) CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error) CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error) + CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error) CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error) CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error) CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error @@ -62,6 +64,7 @@ type Querier interface { DeleteCronJob(ctx context.Context, id uuid.UUID) error DeleteDatabaseServiceByID(ctx context.Context, id string) error DeleteDatabaseServiceByIDAndUser(ctx context.Context, arg DeleteDatabaseServiceByIDAndUserParams) error + DeleteGitProvider(ctx context.Context, arg DeleteGitProviderParams) (int64, error) DeleteNotificationChannel(ctx context.Context, arg DeleteNotificationChannelParams) (int64, error) DeleteOutboundWebhookByIDAndUser(ctx context.Context, arg DeleteOutboundWebhookByIDAndUserParams) error DeletePreviewEnvironment(ctx context.Context, id uuid.UUID) error @@ -95,6 +98,15 @@ type Querier interface { GetDeploymentAccess(ctx context.Context, id uuid.UUID) (GetDeploymentAccessRow, error) GetDeploymentForRollback(ctx context.Context, id uuid.UUID) (GetDeploymentForRollbackRow, error) GetDeploymentWithProject(ctx context.Context, id uuid.UUID) (GetDeploymentWithProjectRow, error) + GetGitProviderForFetch(ctx context.Context, arg GetGitProviderForFetchParams) (GetGitProviderForFetchRow, error) + GetGitProviderForRepos(ctx context.Context, arg GetGitProviderForReposParams) (GetGitProviderForReposRow, error) + GetGitProviderName(ctx context.Context, arg GetGitProviderNameParams) (string, error) + GetGitProviderNameByID(ctx context.Context, id uuid.UUID) (string, error) + GetGitProviderTimestamps(ctx context.Context, id uuid.UUID) (GetGitProviderTimestampsRow, error) + GetGitRepoForPush(ctx context.Context, id uuid.UUID) (GetGitRepoForPushRow, error) + GetGitRepositoryByFullName(ctx context.Context, arg GetGitRepositoryByFullNameParams) (GetGitRepositoryByFullNameRow, error) + GetGitRepositoryProviderID(ctx context.Context, arg GetGitRepositoryProviderIDParams) (uuid.UUID, error) + GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error) GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error) GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error) GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error) @@ -132,6 +144,8 @@ type Querier interface { InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error InsertCronJob(ctx context.Context, arg InsertCronJobParams) error InsertDeployment(ctx context.Context, arg InsertDeploymentParams) error + InsertGitProvider(ctx context.Context, arg InsertGitProviderParams) error + InsertGitRepository(ctx context.Context, arg InsertGitRepositoryParams) (InsertGitRepositoryRow, error) InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error) InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error @@ -144,6 +158,7 @@ type Querier interface { ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error) ListBanners(ctx context.Context) ([]Banner, error) ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error) + ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error) ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error) ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error) ListDatabaseBackupsByDatabaseAndUser(ctx context.Context, arg ListDatabaseBackupsByDatabaseAndUserParams) ([]DatabaseBackup, error) @@ -154,6 +169,8 @@ type Querier interface { ListDueDatabaseBackups(ctx context.Context) ([]ListDueDatabaseBackupsRow, error) ListEnabledOutboundWebhooks(ctx context.Context) ([]OutboundWebhook, error) ListExpiredPreviewsForUser(ctx context.Context, arg ListExpiredPreviewsForUserParams) ([]ListExpiredPreviewsForUserRow, error) + ListGitProviders(ctx context.Context, userID uuid.UUID) ([]ListGitProvidersRow, error) + ListGitRepositories(ctx context.Context, arg ListGitRepositoriesParams) ([]ListGitRepositoriesRow, error) ListInstanceMetrics(ctx context.Context, arg ListInstanceMetricsParams) ([]ListInstanceMetricsRow, error) ListNodeAgentsForService(ctx context.Context, serviceID string) ([]string, error) ListNodeMetrics(ctx context.Context, arg ListNodeMetricsParams) ([]ListNodeMetricsRow, error) @@ -185,6 +202,7 @@ type Querier interface { ListServiceTemplatesForUser(ctx context.Context, ownerID uuid.NullUUID) ([]ServiceTemplate, error) ListServiceVariableValues(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariableValuesRow, error) ListServiceVariablesWithSecret(ctx context.Context, serviceID uuid.UUID) ([]ListServiceVariablesWithSecretRow, error) + ListServicesForPush(ctx context.Context, arg ListServicesForPushParams) ([]ListServicesForPushRow, error) ListSleepCandidates(ctx context.Context) ([]ListSleepCandidatesRow, error) ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error) ListUserInvites(ctx context.Context) ([]UserInvite, error) @@ -253,6 +271,9 @@ type Querier interface { UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error) UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error + UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error) + UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error + UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error) UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error diff --git a/app/backend/sqlc/queries/git.sql b/app/backend/sqlc/queries/git.sql new file mode 100644 index 0000000..496acaa --- /dev/null +++ b/app/backend/sqlc/queries/git.sql @@ -0,0 +1,129 @@ +-- name: ListGitProviders :many +SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at +FROM git_providers +WHERE user_id = $1 +ORDER BY created_at DESC; + +-- name: UpsertGitHubAppProvider :one +INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) +ON CONFLICT (name, user_id) +DO UPDATE SET + display_name = EXCLUDED.display_name, + api_url = EXCLUDED.api_url, + webhook_url = EXCLUDED.webhook_url, + access_token = EXCLUDED.access_token, + updated_at = NOW() +RETURNING id, created_at, updated_at; + +-- name: InsertGitProvider :exec +INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()); + +-- name: GetGitProviderTimestamps :one +SELECT created_at, updated_at FROM git_providers WHERE id = $1; + +-- name: GetGitProviderForRepos :one +SELECT name, display_name, access_token, api_url +FROM git_providers +WHERE id = $1 AND user_id = $2; + +-- name: GetGitProviderName :one +SELECT name FROM git_providers WHERE id = $1 AND user_id = $2; + +-- name: GetGitProviderForFetch :one +SELECT name, api_url, access_token +FROM git_providers +WHERE id = $1 AND user_id = $2; + +-- name: DeleteGitProvider :execrows +DELETE FROM git_providers WHERE id = $1 AND user_id = $2; + +-- name: ListGitRepositories :many +SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description, + clone_url, COALESCE(webhook_url, '') AS webhook_url, + default_branch, is_private, user_id, created_at, updated_at +FROM git_repositories +WHERE provider_id = $1 AND user_id = $2 + AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%')) +ORDER BY updated_at DESC +LIMIT $4 OFFSET $5; + +-- name: CountGitRepositories :one +SELECT COUNT(*) +FROM git_repositories +WHERE provider_id = $1 AND user_id = $2 + AND ($3::text IS NULL OR full_name ILIKE ('%' || $3::text || '%') OR name ILIKE ('%' || $3::text || '%')); + +-- name: GetGitRepositoryByFullName :one +SELECT id, provider_id, name, full_name, COALESCE(description, '') AS description, + clone_url, COALESCE(webhook_url, '') AS webhook_url, + default_branch, is_private, user_id, created_at, updated_at +FROM git_repositories +WHERE provider_id = $1 AND full_name = $2 AND user_id = $3; + +-- name: InsertGitRepository :one +INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) +RETURNING created_at, updated_at; + +-- name: UpsertGitRepository :exec +INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url, default_branch, is_private, user_id, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) +ON CONFLICT (provider_id, full_name) +DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + clone_url = EXCLUDED.clone_url, + default_branch = EXCLUDED.default_branch, + is_private = EXCLUDED.is_private, + updated_at = NOW(); + +-- name: GetGitRepositoryProviderID :one +SELECT provider_id FROM git_repositories WHERE id = $1 AND user_id = $2; + +-- name: UpsertGitWebhook :one +INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, active, branch_filter, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, TRUE, $6, NOW(), NOW()) +ON CONFLICT (repo_id, provider_id) +DO UPDATE SET + events = EXCLUDED.events, + webhook_secret = EXCLUDED.webhook_secret, + active = TRUE, + branch_filter = EXCLUDED.branch_filter, + updated_at = NOW() +RETURNING id, active, created_at, updated_at; + +-- name: ListConnectedRepositories :many +SELECT r.id, r.provider_id, r.name, r.full_name, COALESCE(r.description, '') AS description, r.clone_url, + r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at, + p.name AS provider_name, p.display_name +FROM git_repositories r +JOIN git_providers p ON r.provider_id = p.id +WHERE r.user_id = $1 +ORDER BY r.updated_at DESC +LIMIT $2 OFFSET $3; + +-- name: CountUserRepositories :one +SELECT COUNT(*) FROM git_repositories WHERE user_id = $1; + +-- name: GetGitWebhookForPush :one +SELECT webhook_secret, repo_id, provider_id, COALESCE(branch_filter, '') AS branch_filter, active +FROM git_webhooks WHERE id = $1; + +-- name: GetGitProviderNameByID :one +SELECT name FROM git_providers WHERE id = $1; + +-- name: GetGitRepoForPush :one +SELECT clone_url, full_name, user_id FROM git_repositories WHERE id = $1; + +-- name: ListServicesForPush :many +SELECT s.id, s.project_id, s.name, + COALESCE(s.type, s.service_type, '') AS type, COALESCE(s.status, '') AS status, + COALESCE(s.image, s.image_name, '') AS image, COALESCE(s.command, s.start_command, '') AS command, + COALESCE(s.environment, '') AS environment, COALESCE(s.git_repo, s.source_url, '') AS git_repo, + COALESCE(s.git_branch, '') AS git_branch, COALESCE(s.build_path, '') AS build_path, + COALESCE(s.cpu, '') AS cpu, COALESCE(s.memory, '') AS memory, + s.created_at, s.updated_at +FROM services s +WHERE (s.git_branch = $1 OR $1 = '') AND (s.git_repo = $2 OR s.git_repo = $3); diff --git a/app/backend/sqlc/schema.sql b/app/backend/sqlc/schema.sql index 6d87a53..e826b8b 100644 --- a/app/backend/sqlc/schema.sql +++ b/app/backend/sqlc/schema.sql @@ -489,3 +489,43 @@ CREATE TABLE cron_executions ( output TEXT, error TEXT ); + +CREATE TABLE git_providers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + display_name VARCHAR(255) NOT NULL, + api_url VARCHAR(255) NOT NULL, + webhook_url VARCHAR(255) NOT NULL, + access_token TEXT NOT NULL, + user_id UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE TABLE git_repositories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + provider_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + description TEXT, + clone_url VARCHAR(255) NOT NULL, + webhook_url VARCHAR(255), + default_branch VARCHAR(255) DEFAULT 'main', + is_private BOOLEAN DEFAULT false, + user_id UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE TABLE git_webhooks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + repo_id UUID NOT NULL, + provider_id UUID NOT NULL, + events TEXT NOT NULL, + webhook_secret TEXT NOT NULL, + remote_webhook_id VARCHAR(255), + active BOOLEAN DEFAULT true, + branch_filter VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +);