mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
400 lines
9.6 KiB
Go
400 lines
9.6 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// HoldCaptainRecord represents a cached captain record from a hold's PDS
|
|
type HoldCaptainRecord struct {
|
|
HoldDID string `json:"-"` // Set manually, not from JSON
|
|
OwnerDID string `json:"owner"`
|
|
Public bool `json:"public"`
|
|
AllowAllCrew bool `json:"allowAllCrew"`
|
|
DeployedAt string `json:"deployedAt"`
|
|
Region string `json:"region"`
|
|
UpdatedAt time.Time `json:"-"` // Set manually, not from JSON
|
|
}
|
|
|
|
// GetCaptainRecord retrieves a captain record from the cache
|
|
// Returns nil if not found (cache miss)
|
|
func GetCaptainRecord(db *sql.DB, holdDID string) (*HoldCaptainRecord, error) {
|
|
query := `
|
|
SELECT hold_did, owner_did, public, allow_all_crew,
|
|
deployed_at, region, updated_at
|
|
FROM hold_captain_records
|
|
WHERE hold_did = ?
|
|
`
|
|
|
|
var record HoldCaptainRecord
|
|
var deployedAt, region sql.NullString
|
|
|
|
err := db.QueryRow(query, holdDID).Scan(
|
|
&record.HoldDID,
|
|
&record.OwnerDID,
|
|
&record.Public,
|
|
&record.AllowAllCrew,
|
|
&deployedAt,
|
|
®ion,
|
|
&record.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil // Cache miss - not an error
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query captain record: %w", err)
|
|
}
|
|
|
|
// Handle nullable fields
|
|
if deployedAt.Valid {
|
|
record.DeployedAt = deployedAt.String
|
|
}
|
|
if region.Valid {
|
|
record.Region = region.String
|
|
}
|
|
|
|
return &record, nil
|
|
}
|
|
|
|
// UpsertCaptainRecord inserts or updates a captain record in the cache
|
|
func UpsertCaptainRecord(db *sql.DB, record *HoldCaptainRecord) error {
|
|
query := `
|
|
INSERT INTO hold_captain_records (
|
|
hold_did, owner_did, public, allow_all_crew,
|
|
deployed_at, region, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(hold_did) DO UPDATE SET
|
|
owner_did = excluded.owner_did,
|
|
public = excluded.public,
|
|
allow_all_crew = excluded.allow_all_crew,
|
|
deployed_at = excluded.deployed_at,
|
|
region = excluded.region,
|
|
updated_at = excluded.updated_at
|
|
`
|
|
|
|
_, err := db.Exec(query,
|
|
record.HoldDID,
|
|
record.OwnerDID,
|
|
record.Public,
|
|
record.AllowAllCrew,
|
|
nullString(record.DeployedAt),
|
|
nullString(record.Region),
|
|
record.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upsert captain record: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListHoldDIDs returns all known hold DIDs from the cache
|
|
func ListHoldDIDs(db *sql.DB) ([]string, error) {
|
|
query := `
|
|
SELECT hold_did
|
|
FROM hold_captain_records
|
|
ORDER BY updated_at DESC
|
|
`
|
|
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query hold DIDs: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holdDIDs []string
|
|
for rows.Next() {
|
|
var holdDID string
|
|
if err := rows.Scan(&holdDID); err != nil {
|
|
return nil, fmt.Errorf("failed to scan hold DID: %w", err)
|
|
}
|
|
holdDIDs = append(holdDIDs, holdDID)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating hold DIDs: %w", err)
|
|
}
|
|
|
|
return holdDIDs, nil
|
|
}
|
|
|
|
// nullString converts a string to sql.NullString
|
|
func nullString(s string) sql.NullString {
|
|
if s == "" {
|
|
return sql.NullString{Valid: false}
|
|
}
|
|
return sql.NullString{String: s, Valid: true}
|
|
}
|
|
|
|
// GetCaptainRecordsForOwner retrieves all captain records where the user is the owner
|
|
// Used for GDPR export to find all holds owned by a user
|
|
func GetCaptainRecordsForOwner(db *sql.DB, ownerDID string) ([]*HoldCaptainRecord, error) {
|
|
query := `
|
|
SELECT hold_did, owner_did, public, allow_all_crew,
|
|
deployed_at, region, updated_at
|
|
FROM hold_captain_records
|
|
WHERE owner_did = ?
|
|
ORDER BY updated_at DESC
|
|
`
|
|
|
|
rows, err := db.Query(query, ownerDID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query captain records for owner: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var records []*HoldCaptainRecord
|
|
for rows.Next() {
|
|
var record HoldCaptainRecord
|
|
var deployedAt, region sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&record.HoldDID,
|
|
&record.OwnerDID,
|
|
&record.Public,
|
|
&record.AllowAllCrew,
|
|
&deployedAt,
|
|
®ion,
|
|
&record.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan captain record: %w", err)
|
|
}
|
|
|
|
if deployedAt.Valid {
|
|
record.DeployedAt = deployedAt.String
|
|
}
|
|
if region.Valid {
|
|
record.Region = region.String
|
|
}
|
|
|
|
records = append(records, &record)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating captain records: %w", err)
|
|
}
|
|
|
|
if records == nil {
|
|
records = []*HoldCaptainRecord{}
|
|
}
|
|
|
|
return records, nil
|
|
}
|
|
|
|
// DeleteCaptainRecord removes a captain record from the cache
|
|
func DeleteCaptainRecord(db *sql.DB, holdDID string) error {
|
|
// Note: hold_crew_members doesn't have CASCADE, so delete crew first
|
|
_, err := db.Exec(`DELETE FROM hold_crew_members WHERE hold_did = ?`, holdDID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete crew members for hold: %w", err)
|
|
}
|
|
|
|
_, err = db.Exec(`DELETE FROM hold_captain_records WHERE hold_did = ?`, holdDID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete captain record: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CrewMember represents a cached crew membership from Jetstream
|
|
type CrewMember struct {
|
|
HoldDID string
|
|
MemberDID string
|
|
Rkey string
|
|
Role string
|
|
Permissions string // JSON array
|
|
Tier string
|
|
AddedAt string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// UpsertCrewMember inserts or updates a crew member record
|
|
func UpsertCrewMember(db *sql.DB, member *CrewMember) error {
|
|
query := `
|
|
INSERT INTO hold_crew_members (
|
|
hold_did, member_did, rkey, role, permissions, tier, added_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(hold_did, member_did) DO UPDATE SET
|
|
rkey = excluded.rkey,
|
|
role = excluded.role,
|
|
permissions = excluded.permissions,
|
|
tier = excluded.tier,
|
|
added_at = excluded.added_at,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
`
|
|
|
|
_, err := db.Exec(query,
|
|
member.HoldDID,
|
|
member.MemberDID,
|
|
member.Rkey,
|
|
nullString(member.Role),
|
|
nullString(member.Permissions),
|
|
nullString(member.Tier),
|
|
nullString(member.AddedAt),
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upsert crew member: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteCrewMemberByRkey removes a crew member by rkey (for delete events from Jetstream)
|
|
func DeleteCrewMemberByRkey(db *sql.DB, holdDID, rkey string) error {
|
|
_, err := db.Exec(`DELETE FROM hold_crew_members WHERE hold_did = ? AND rkey = ?`, holdDID, rkey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete crew member by rkey: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AvailableHold represents a hold available to a user, with membership info
|
|
type AvailableHold struct {
|
|
HoldDID string
|
|
OwnerDID string
|
|
Public bool
|
|
AllowAllCrew bool
|
|
Region string
|
|
Membership string // "owner", "crew", "eligible", "public"
|
|
Permissions string // JSON array (only for crew)
|
|
}
|
|
|
|
// GetAvailableHolds returns all holds available to a user, grouped by membership type
|
|
// Results are ordered: owner first, then crew, then eligible, then public
|
|
func GetAvailableHolds(db *sql.DB, userDID string) ([]AvailableHold, error) {
|
|
query := `
|
|
SELECT
|
|
h.hold_did,
|
|
h.owner_did,
|
|
h.public,
|
|
h.allow_all_crew,
|
|
h.region,
|
|
CASE
|
|
WHEN h.owner_did = ?1 THEN 'owner'
|
|
WHEN c.member_did IS NOT NULL THEN 'crew'
|
|
WHEN h.allow_all_crew = 1 THEN 'eligible'
|
|
WHEN h.public = 1 THEN 'public'
|
|
ELSE 'none'
|
|
END as membership,
|
|
c.permissions
|
|
FROM hold_captain_records h
|
|
LEFT JOIN hold_crew_members c ON h.hold_did = c.hold_did AND c.member_did = ?1
|
|
WHERE h.public = 1
|
|
OR h.allow_all_crew = 1
|
|
OR h.owner_did = ?1
|
|
OR c.member_did IS NOT NULL
|
|
ORDER BY
|
|
CASE
|
|
WHEN h.owner_did = ?1 THEN 0
|
|
WHEN c.member_did IS NOT NULL THEN 1
|
|
WHEN h.allow_all_crew = 1 THEN 2
|
|
ELSE 3
|
|
END,
|
|
h.hold_did
|
|
`
|
|
|
|
rows, err := db.Query(query, userDID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query available holds: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var holds []AvailableHold
|
|
for rows.Next() {
|
|
var hold AvailableHold
|
|
var region, permissions sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&hold.HoldDID,
|
|
&hold.OwnerDID,
|
|
&hold.Public,
|
|
&hold.AllowAllCrew,
|
|
®ion,
|
|
&hold.Membership,
|
|
&permissions,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan available hold: %w", err)
|
|
}
|
|
|
|
if region.Valid {
|
|
hold.Region = region.String
|
|
}
|
|
if permissions.Valid {
|
|
hold.Permissions = permissions.String
|
|
}
|
|
|
|
holds = append(holds, hold)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating available holds: %w", err)
|
|
}
|
|
|
|
return holds, nil
|
|
}
|
|
|
|
// GetCrewMemberships returns all holds where a user is a crew member
|
|
func GetCrewMemberships(db *sql.DB, memberDID string) ([]CrewMember, error) {
|
|
query := `
|
|
SELECT hold_did, member_did, rkey, role, permissions, tier, added_at, created_at, updated_at
|
|
FROM hold_crew_members
|
|
WHERE member_did = ?
|
|
ORDER BY added_at DESC
|
|
`
|
|
|
|
rows, err := db.Query(query, memberDID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query crew memberships: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var memberships []CrewMember
|
|
for rows.Next() {
|
|
var m CrewMember
|
|
var role, permissions, tier, addedAt sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&m.HoldDID,
|
|
&m.MemberDID,
|
|
&m.Rkey,
|
|
&role,
|
|
&permissions,
|
|
&tier,
|
|
&addedAt,
|
|
&m.CreatedAt,
|
|
&m.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan crew membership: %w", err)
|
|
}
|
|
|
|
if role.Valid {
|
|
m.Role = role.String
|
|
}
|
|
if permissions.Valid {
|
|
m.Permissions = permissions.String
|
|
}
|
|
if tier.Valid {
|
|
m.Tier = tier.String
|
|
}
|
|
if addedAt.Valid {
|
|
m.AddedAt = addedAt.String
|
|
}
|
|
|
|
memberships = append(memberships, m)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating crew memberships: %w", err)
|
|
}
|
|
|
|
return memberships, nil
|
|
}
|