Files
at-container-registry/pkg/appview/db/hold_store.go
T

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,
&region,
&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,
&region,
&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,
&region,
&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
}