Monorepo for Tangled
at master 270 lines 7.0 kB view raw
1package db 2 3import ( 4 "database/sql" 5 "errors" 6 "fmt" 7 "time" 8 9 "tangled.org/core/appview/models" 10) 11 12var ( 13 ErrDomainTaken = errors.New("domain is already claimed by another user") 14 ErrDomainCooldown = errors.New("domain is in a 30-day cooldown period after being released") 15 ErrAlreadyClaimed = errors.New("you already have an active domain claim; release it before claiming a new one") 16) 17 18func scanClaim(row *sql.Row) (*models.DomainClaim, error) { 19 var claim models.DomainClaim 20 var deletedStr sql.NullString 21 22 if err := row.Scan(&claim.ID, &claim.Did, &claim.Domain, &deletedStr); err != nil { 23 return nil, err 24 } 25 26 if deletedStr.Valid { 27 t, err := time.Parse(time.RFC3339, deletedStr.String) 28 if err != nil { 29 return nil, fmt.Errorf("parsing deleted timestamp: %w", err) 30 } 31 claim.Deleted = &t 32 } 33 34 return &claim, nil 35} 36 37func GetDomainClaimByDomain(e Execer, domain string) (*models.DomainClaim, error) { 38 row := e.QueryRow(` 39 select id, did, domain, deleted 40 from domain_claims 41 where domain = ? 42 `, domain) 43 return scanClaim(row) 44} 45 46func GetActiveDomainClaimForDid(e Execer, did string) (*models.DomainClaim, error) { 47 row := e.QueryRow(` 48 select id, did, domain, deleted 49 from domain_claims 50 where did = ? and deleted is null 51 `, did) 52 return scanClaim(row) 53} 54 55func GetDomainClaimForDid(e Execer, did string) (*models.DomainClaim, error) { 56 row := e.QueryRow(` 57 select id, did, domain, deleted 58 from domain_claims 59 where did = ? 60 `, did) 61 return scanClaim(row) 62} 63 64func ClaimDomain(e Execer, did, domain string) error { 65 const cooldown = 30 * 24 * time.Hour 66 67 domainRow, err := GetDomainClaimByDomain(e, domain) 68 if err != nil && !errors.Is(err, sql.ErrNoRows) { 69 return fmt.Errorf("looking up domain: %w", err) 70 } 71 72 if domainRow != nil { 73 if domainRow.Did == did { 74 if domainRow.Deleted == nil { 75 return nil 76 } 77 if time.Since(*domainRow.Deleted) < cooldown { 78 return ErrDomainCooldown 79 } 80 _, err = e.Exec(` 81 update domain_claims set deleted = null where did = ? and domain = ? 82 `, did, domain) 83 return err 84 } 85 86 if domainRow.Deleted == nil { 87 return ErrDomainTaken 88 } 89 if time.Since(*domainRow.Deleted) < cooldown { 90 return ErrDomainCooldown 91 } 92 93 if _, err = e.Exec(`delete from domain_claims where domain = ?`, domain); err != nil { 94 return fmt.Errorf("clearing expired domain row: %w", err) 95 } 96 } 97 98 didRow, err := GetDomainClaimForDid(e, did) 99 if err != nil && !errors.Is(err, sql.ErrNoRows) { 100 return fmt.Errorf("looking up DID claim: %w", err) 101 } 102 103 if didRow == nil { 104 _, err = e.Exec(` 105 insert into domain_claims (did, domain) values (?, ?) 106 `, did, domain) 107 return err 108 } 109 110 if didRow.Deleted == nil { 111 return ErrAlreadyClaimed 112 } 113 114 _, err = e.Exec(` 115 update domain_claims set domain = ?, deleted = null where did = ? 116 `, domain, did) 117 return err 118} 119 120func ReleaseDomain(e Execer, did, domain string) error { 121 result, err := e.Exec(` 122 update domain_claims 123 set deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 124 where did = ? and domain = ? and deleted is null 125 `, did, domain) 126 if err != nil { 127 return err 128 } 129 130 n, err := result.RowsAffected() 131 if err != nil { 132 return err 133 } 134 if n == 0 { 135 return errors.New("domain not found or not actively claimed by this account") 136 } 137 return nil 138} 139 140// GetRepoSiteConfig returns the site configuration for a repo, or nil if not configured. 141func GetRepoSiteConfig(e Execer, repoAt string) (*models.RepoSite, error) { 142 row := e.QueryRow(` 143 select id, repo_at, branch, dir, is_index, created, updated 144 from repo_sites 145 where repo_at = ? 146 `, repoAt) 147 148 var s models.RepoSite 149 var isIndex int 150 var createdStr, updatedStr string 151 152 err := row.Scan(&s.ID, &s.RepoAt, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr) 153 if errors.Is(err, sql.ErrNoRows) { 154 return nil, nil 155 } 156 if err != nil { 157 return nil, err 158 } 159 160 s.IsIndex = isIndex != 0 161 162 s.Created, err = time.Parse(time.RFC3339, createdStr) 163 if err != nil { 164 return nil, fmt.Errorf("parsing created timestamp: %w", err) 165 } 166 167 s.Updated, err = time.Parse(time.RFC3339, updatedStr) 168 if err != nil { 169 return nil, fmt.Errorf("parsing updated timestamp: %w", err) 170 } 171 172 return &s, nil 173} 174 175// SetRepoSiteConfig inserts or replaces the site configuration for a repo. 176func SetRepoSiteConfig(e Execer, repoAt, branch, dir string, isIndex bool) error { 177 isIndexInt := 0 178 if isIndex { 179 isIndexInt = 1 180 } 181 182 _, err := e.Exec(` 183 insert into repo_sites (repo_at, branch, dir, is_index, updated) 184 values (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 185 on conflict(repo_at) do update set 186 branch = excluded.branch, 187 dir = excluded.dir, 188 is_index = excluded.is_index, 189 updated = excluded.updated 190 `, repoAt, branch, dir, isIndexInt) 191 return err 192} 193 194// DeleteRepoSiteConfig removes the site configuration for a repo. 195func DeleteRepoSiteConfig(e Execer, repoAt string) error { 196 _, err := e.Exec(`delete from repo_sites where repo_at = ?`, repoAt) 197 return err 198} 199 200// GetRepoSiteConfigsForDid returns all site configurations for repos owned by a DID. 201// RepoName is populated on each returned RepoSite. 202func GetRepoSiteConfigsForDid(e Execer, did string) ([]*models.RepoSite, error) { 203 rows, err := e.Query(` 204 select rs.id, rs.repo_at, r.name, rs.branch, rs.dir, rs.is_index, rs.created, rs.updated 205 from repo_sites rs 206 join repos r on r.at_uri = rs.repo_at 207 where r.did = ? 208 `, did) 209 if err != nil { 210 return nil, err 211 } 212 defer rows.Close() 213 214 var sites []*models.RepoSite 215 for rows.Next() { 216 var s models.RepoSite 217 var isIndex int 218 var createdStr, updatedStr string 219 if err := rows.Scan(&s.ID, &s.RepoAt, &s.RepoName, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr); err != nil { 220 return nil, err 221 } 222 s.IsIndex = isIndex != 0 223 s.Created, err = time.Parse(time.RFC3339, createdStr) 224 if err != nil { 225 return nil, fmt.Errorf("parsing created timestamp: %w", err) 226 } 227 s.Updated, err = time.Parse(time.RFC3339, updatedStr) 228 if err != nil { 229 return nil, fmt.Errorf("parsing updated timestamp: %w", err) 230 } 231 sites = append(sites, &s) 232 } 233 return sites, rows.Err() 234} 235 236// DeleteRepoSiteConfigsForDid removes all site configurations for repos owned by a DID. 237func DeleteRepoSiteConfigsForDid(e Execer, did string) error { 238 _, err := e.Exec(` 239 delete from repo_sites 240 where repo_at in ( 241 select at_uri from repos where did = ? 242 ) 243 `, did) 244 return err 245} 246 247// GetIndexRepoAtForDid returns the repo_at of the repo that currently holds 248// is_index=1 for the given DID, excluding excludeRepoAt (the current repo). 249// Returns "", nil if no other repo is the index site. 250func GetIndexRepoAtForDid(e Execer, did, excludeRepoAt string) (string, error) { 251 row := e.QueryRow(` 252 select rs.repo_at 253 from repo_sites rs 254 join repos r on r.at_uri = rs.repo_at 255 where r.did = ? 256 and rs.is_index = 1 257 and rs.repo_at != ? 258 limit 1 259 `, did, excludeRepoAt) 260 261 var repoAt string 262 err := row.Scan(&repoAt) 263 if errors.Is(err, sql.ErrNoRows) { 264 return "", nil 265 } 266 if err != nil { 267 return "", err 268 } 269 return repoAt, nil 270}