diff --git a/appview/db/db.go b/appview/db/db.go --- a/appview/db/db.go +++ b/appview/db/db.go @@ -616,6 +616,30 @@ domain text not null unique, deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed ); + create table if not exists repo_sites ( + id integer primary key autoincrement, + repo_at text not null unique, + branch text not null, + dir text not null default '/', + is_index integer not null default 0, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + foreign key (repo_at) references repos(at_uri) on delete cascade + ); + + create table if not exists site_deploys ( + id integer primary key autoincrement, + repo_at text not null, + branch text not null, + dir text not null default '/', + commit_sha text not null default '', + status text not null check (status in ('success', 'failure')), + trigger text not null check (trigger in ('config_change', 'push')), + error text not null default '', + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + foreign key (repo_at) references repos(at_uri) on delete cascade + ); + create table if not exists migrations ( id integer primary key autoincrement, name text unique @@ -635,6 +659,7 @@ create index if not exists idx_references_from_at on reference_links(from_at); create index if not exists idx_references_to_at on reference_links(to_at); create index if not exists idx_webhooks_repo_at on webhooks(repo_at); create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id); + create index if not exists idx_site_deploys_repo_at on site_deploys(repo_at); `) if err != nil { return nil, err @@ -1258,22 +1283,6 @@ drop table profile_stats; -- rename new table alter table profile_stats_new rename to profile_stats; - `) - return err - }) - - orm.RunMigration(conn, logger, "add-repo-sites-table", func(tx *sql.Tx) error { - _, err := tx.Exec(` - create table if not exists repo_sites ( - id integer primary key autoincrement, - repo_at text not null unique, - branch text not null, - dir text not null default '/', - is_index integer not null default 0, - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - foreign key (repo_at) references repos(at_uri) on delete cascade - ); `) return err }) diff --git a/appview/db/site_deploys.go b/appview/db/site_deploys.go new file mode 100644 --- /dev/null +++ b/appview/db/site_deploys.go @@ -0,0 +1,102 @@ +package db + +import ( + "fmt" + "time" + + "tangled.org/core/appview/models" +) + +// AddSiteDeploy records a site deploy attempt. +func AddSiteDeploy(e Execer, deploy *models.SiteDeploy) error { + result, err := e.Exec(` + insert into site_deploys ( + repo_at, + branch, + dir, + commit_sha, + status, + trigger, + error + ) values (?, ?, ?, ?, ?, ?, ?) + `, + deploy.RepoAt, + deploy.Branch, + deploy.Dir, + deploy.CommitSHA, + string(deploy.Status), + string(deploy.Trigger), + deploy.Error, + ) + if err != nil { + return fmt.Errorf("failed to insert site deploy: %w", err) + } + + id, err := result.LastInsertId() + if err != nil { + return fmt.Errorf("failed to get site deploy id: %w", err) + } + + deploy.Id = id + return nil +} + +// GetSiteDeploys returns recent deploy records for a repository, newest first. +func GetSiteDeploys(e Execer, repoAt string, limit int) ([]models.SiteDeploy, error) { + if limit <= 0 { + limit = 20 + } + + rows, err := e.Query(` + select + id, + repo_at, + branch, + dir, + commit_sha, + status, + trigger, + error, + created_at + from site_deploys + where repo_at = ? + order by created_at desc + limit ? + `, repoAt, limit) + if err != nil { + return nil, fmt.Errorf("failed to query site deploys: %w", err) + } + defer rows.Close() + + var deploys []models.SiteDeploy + for rows.Next() { + var d models.SiteDeploy + var createdAt string + + if err := rows.Scan( + &d.Id, + &d.RepoAt, + &d.Branch, + &d.Dir, + &d.CommitSHA, + &d.Status, + &d.Trigger, + &d.Error, + &createdAt, + ); err != nil { + return nil, fmt.Errorf("failed to scan site deploy: %w", err) + } + + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { + d.CreatedAt = t + } + + deploys = append(deploys, d) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("failed to iterate site deploys: %w", err) + } + + return deploys, nil +} diff --git a/appview/db/sites.go b/appview/db/sites.go --- a/appview/db/sites.go +++ b/appview/db/sites.go @@ -196,3 +196,75 @@ func DeleteRepoSiteConfig(e Execer, repoAt string) error { _, err := e.Exec(`delete from repo_sites where repo_at = ?`, repoAt) return err } + +// GetRepoSiteConfigsForDid returns all site configurations for repos owned by a DID. +// RepoName is populated on each returned RepoSite. +func GetRepoSiteConfigsForDid(e Execer, did string) ([]*models.RepoSite, error) { + rows, err := e.Query(` + select rs.id, rs.repo_at, r.name, rs.branch, rs.dir, rs.is_index, rs.created, rs.updated + from repo_sites rs + join repos r on r.at_uri = rs.repo_at + where r.did = ? + `, did) + if err != nil { + return nil, err + } + defer rows.Close() + + var sites []*models.RepoSite + for rows.Next() { + var s models.RepoSite + var isIndex int + var createdStr, updatedStr string + if err := rows.Scan(&s.ID, &s.RepoAt, &s.RepoName, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr); err != nil { + return nil, err + } + s.IsIndex = isIndex != 0 + s.Created, err = time.Parse(time.RFC3339, createdStr) + if err != nil { + return nil, fmt.Errorf("parsing created timestamp: %w", err) + } + s.Updated, err = time.Parse(time.RFC3339, updatedStr) + if err != nil { + return nil, fmt.Errorf("parsing updated timestamp: %w", err) + } + sites = append(sites, &s) + } + return sites, rows.Err() +} + +// DeleteRepoSiteConfigsForDid removes all site configurations for repos owned by a DID. +func DeleteRepoSiteConfigsForDid(e Execer, did string) error { + _, err := e.Exec(` + delete from repo_sites + where repo_at in ( + select at_uri from repos where did = ? + ) + `, did) + return err +} + +// GetIndexRepoAtForDid returns the repo_at of the repo that currently holds +// is_index=1 for the given DID, excluding excludeRepoAt (the current repo). +// Returns "", nil if no other repo is the index site. +func GetIndexRepoAtForDid(e Execer, did, excludeRepoAt string) (string, error) { + row := e.QueryRow(` + select rs.repo_at + from repo_sites rs + join repos r on r.at_uri = rs.repo_at + where r.did = ? + and rs.is_index = 1 + and rs.repo_at != ? + limit 1 + `, did, excludeRepoAt) + + var repoAt string + err := row.Scan(&repoAt) + if errors.Is(err, sql.ErrNoRows) { + return "", nil + } + if err != nil { + return "", err + } + return repoAt, nil +} diff --git a/appview/models/site_deploy.go b/appview/models/site_deploy.go new file mode 100644 --- /dev/null +++ b/appview/models/site_deploy.go @@ -0,0 +1,40 @@ +package models + +import "time" + +type SiteDeployStatus string + +const ( + SiteDeployStatusSuccess SiteDeployStatus = "success" + SiteDeployStatusFailure SiteDeployStatus = "failure" +) + +type SiteDeployTrigger string + +const ( + SiteDeployTriggerConfigChange SiteDeployTrigger = "config_change" + SiteDeployTriggerPush SiteDeployTrigger = "push" +) + +func (t SiteDeployTrigger) Label() string { + switch t { + case SiteDeployTriggerConfigChange: + return "config change" + case SiteDeployTriggerPush: + return "push" + default: + return string(t) + } +} + +type SiteDeploy struct { + Id int64 + RepoAt string + Branch string + Dir string + CommitSHA string + Status SiteDeployStatus + Trigger SiteDeployTrigger + Error string + CreatedAt time.Time +} diff --git a/appview/models/sites.go b/appview/models/sites.go --- a/appview/models/sites.go +++ b/appview/models/sites.go @@ -10,11 +10,12 @@ Deleted *time.Time } type RepoSite struct { - ID int64 - RepoAt string - Branch string - Dir string - IsIndex bool - Created time.Time - Updated time.Time + ID int64 + RepoAt string + RepoName string // populated when joined with repos table + Branch string + Dir string + IsIndex bool + Created time.Time + Updated time.Time }