A community based topic aggregation platform built on atproto
1package carstore
2
3import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/bluesky-social/indigo/models"
9 "gorm.io/gorm"
10)
11
12// UserMapping manages the mapping between DIDs and numeric UIDs required by Indigo's carstore
13type UserMapping struct {
14 db *gorm.DB
15 mu sync.RWMutex
16 didToUID map[string]models.Uid
17 uidToDID map[models.Uid]string
18 nextUID models.Uid
19}
20
21// UserMap represents the database model for DID to UID mapping
22type UserMap struct {
23 UID models.Uid `gorm:"primaryKey;autoIncrement"`
24 DID string `gorm:"column:did;uniqueIndex;not null"`
25 CreatedAt int64
26 UpdatedAt int64
27}
28
29// NewUserMapping creates a new UserMapping instance
30func NewUserMapping(db *gorm.DB) (*UserMapping, error) {
31 // Auto-migrate the user mapping table
32 if err := db.AutoMigrate(&UserMap{}); err != nil {
33 return nil, fmt.Errorf("migrating user mapping table: %w", err)
34 }
35
36 um := &UserMapping{
37 db: db,
38 didToUID: make(map[string]models.Uid),
39 uidToDID: make(map[models.Uid]string),
40 nextUID: 1,
41 }
42
43 // Load existing mappings
44 if err := um.loadMappings(); err != nil {
45 return nil, fmt.Errorf("loading user mappings: %w", err)
46 }
47
48 return um, nil
49}
50
51// loadMappings loads all existing DID to UID mappings from the database
52func (um *UserMapping) loadMappings() error {
53 var mappings []UserMap
54 if err := um.db.Find(&mappings).Error; err != nil {
55 return fmt.Errorf("querying user mappings: %w", err)
56 }
57
58 um.mu.Lock()
59 defer um.mu.Unlock()
60
61 for _, m := range mappings {
62 um.didToUID[m.DID] = m.UID
63 um.uidToDID[m.UID] = m.DID
64 if m.UID >= um.nextUID {
65 um.nextUID = m.UID + 1
66 }
67 }
68
69 return nil
70}
71
72// GetOrCreateUID gets or creates a UID for a given DID
73func (um *UserMapping) GetOrCreateUID(ctx context.Context, did string) (models.Uid, error) {
74 um.mu.RLock()
75 if uid, exists := um.didToUID[did]; exists {
76 um.mu.RUnlock()
77 return uid, nil
78 }
79 um.mu.RUnlock()
80
81 // Need to create a new mapping
82 um.mu.Lock()
83 defer um.mu.Unlock()
84
85 // Double-check in case another goroutine created it
86 if uid, exists := um.didToUID[did]; exists {
87 return uid, nil
88 }
89
90 // Create new mapping
91 userMap := &UserMap{
92 DID: did,
93 }
94
95 if err := um.db.Create(userMap).Error; err != nil {
96 return 0, fmt.Errorf("creating user mapping for DID %s: %w", did, err)
97 }
98
99 um.didToUID[did] = userMap.UID
100 um.uidToDID[userMap.UID] = did
101
102 return userMap.UID, nil
103}
104
105// GetUID returns the UID for a DID, or an error if not found
106func (um *UserMapping) GetUID(did string) (models.Uid, error) {
107 um.mu.RLock()
108 defer um.mu.RUnlock()
109
110 uid, exists := um.didToUID[did]
111 if !exists {
112 return 0, fmt.Errorf("UID not found for DID: %s", did)
113 }
114 return uid, nil
115}
116
117// GetDID returns the DID for a UID, or an error if not found
118func (um *UserMapping) GetDID(uid models.Uid) (string, error) {
119 um.mu.RLock()
120 defer um.mu.RUnlock()
121
122 did, exists := um.uidToDID[uid]
123 if !exists {
124 return "", fmt.Errorf("DID not found for UID: %d", uid)
125 }
126 return did, nil
127}