Lanyards is a dedicated profile for researchers, built on the AT Protocol.
at main 275 lines 7.6 kB view raw
1/** 2 * Repository pattern for managing profile data in PDS 3 * This provides an abstraction layer for CRUD operations on AT Protocol records 4 */ 5 6import { AtpAgent } from '@atproto/api'; 7import { TID } from '@atproto/common'; 8import type { 9 Profile, 10 Affiliation, 11 Link as WebLink, 12 Work, 13 Event, 14} from '@/types'; 15 16const LEXICON_PREFIX = 'at.lanyard'; 17 18export class ProfileRepository { 19 constructor(private agent: AtpAgent) {} 20 21 // Profile operations 22 async getProfile(did: string): Promise<Profile | null> { 23 try { 24 const response = await this.agent.com.atproto.repo.getRecord({ 25 repo: did, 26 collection: `${LEXICON_PREFIX}.profile`, 27 rkey: 'self', 28 }); 29 return response.data.value as unknown as Profile; 30 } catch { 31 return null; 32 } 33 } 34 35 async createProfile(profile: Omit<Profile, 'createdAt'>) { 36 return this.agent.com.atproto.repo.putRecord({ 37 repo: this.agent.session?.did || '', 38 collection: `${LEXICON_PREFIX}.profile`, 39 rkey: 'self', 40 record: { 41 $type: `${LEXICON_PREFIX}.profile`, 42 ...profile, 43 createdAt: new Date().toISOString(), 44 }, 45 }); 46 } 47 48 async updateProfile(updates: Partial<Profile>) { 49 const current = await this.getProfile(this.agent.session?.did || ''); 50 if (!current) { 51 throw new Error('Profile not found'); 52 } 53 54 return this.agent.com.atproto.repo.putRecord({ 55 repo: this.agent.session?.did || '', 56 collection: `${LEXICON_PREFIX}.profile`, 57 rkey: 'self', 58 record: { 59 ...current, 60 ...updates, 61 $type: `${LEXICON_PREFIX}.profile`, 62 updatedAt: new Date().toISOString(), 63 }, 64 }); 65 } 66 67 // Affiliation operations 68 async listAffiliations(did: string): Promise<Affiliation[]> { 69 const response = await this.agent.com.atproto.repo.listRecords({ 70 repo: did, 71 collection: `${LEXICON_PREFIX}.affiliation`, 72 }); 73 return response.data.records.map((r) => r.value as unknown as Affiliation); 74 } 75 76 async createAffiliation( 77 affiliation: Omit<Affiliation, 'createdAt'> 78 ): Promise<string> { 79 const rkey = TID.nextStr(); 80 await this.agent.com.atproto.repo.putRecord({ 81 repo: this.agent.session?.did || '', 82 collection: `${LEXICON_PREFIX}.affiliation`, 83 rkey, 84 record: { 85 $type: `${LEXICON_PREFIX}.affiliation`, 86 ...affiliation, 87 createdAt: new Date().toISOString(), 88 }, 89 }); 90 return rkey; 91 } 92 93 async updateAffiliation(rkey: string, updates: Partial<Affiliation>) { 94 const record = await this.agent.com.atproto.repo.getRecord({ 95 repo: this.agent.session?.did || '', 96 collection: `${LEXICON_PREFIX}.affiliation`, 97 rkey, 98 }); 99 100 return this.agent.com.atproto.repo.putRecord({ 101 repo: this.agent.session?.did || '', 102 collection: `${LEXICON_PREFIX}.affiliation`, 103 rkey, 104 record: { 105 ...record.data.value, 106 ...updates, 107 $type: `${LEXICON_PREFIX}.affiliation`, 108 }, 109 }); 110 } 111 112 async deleteAffiliation(rkey: string) { 113 return this.agent.com.atproto.repo.deleteRecord({ 114 repo: this.agent.session?.did || '', 115 collection: `${LEXICON_PREFIX}.affiliation`, 116 rkey, 117 }); 118 } 119 120 // WebLink operations (all links: social, academic, and custom web) 121 async listWebLinks(did: string): Promise<WebLink[]> { 122 const response = await this.agent.com.atproto.repo.listRecords({ 123 repo: did, 124 collection: `${LEXICON_PREFIX}.link`, 125 }); 126 return response.data.records.map((r) => r.value as unknown as WebLink); 127 } 128 129 async createWebLink(link: Omit<WebLink, 'createdAt'>): Promise<string> { 130 const rkey = TID.nextStr(); 131 await this.agent.com.atproto.repo.putRecord({ 132 repo: this.agent.session?.did || '', 133 collection: `${LEXICON_PREFIX}.link`, 134 rkey, 135 record: { 136 $type: `${LEXICON_PREFIX}.link`, 137 ...link, 138 createdAt: new Date().toISOString(), 139 }, 140 }); 141 return rkey; 142 } 143 144 async updateWebLink(rkey: string, updates: Partial<WebLink>) { 145 const record = await this.agent.com.atproto.repo.getRecord({ 146 repo: this.agent.session?.did || '', 147 collection: `${LEXICON_PREFIX}.link`, 148 rkey, 149 }); 150 151 return this.agent.com.atproto.repo.putRecord({ 152 repo: this.agent.session?.did || '', 153 collection: `${LEXICON_PREFIX}.link`, 154 rkey, 155 record: { 156 ...record.data.value, 157 ...updates, 158 $type: `${LEXICON_PREFIX}.link`, 159 }, 160 }); 161 } 162 163 async deleteWebLink(rkey: string) { 164 return this.agent.com.atproto.repo.deleteRecord({ 165 repo: this.agent.session?.did || '', 166 collection: `${LEXICON_PREFIX}.link`, 167 rkey, 168 }); 169 } 170 171 // Work operations 172 async listWorks(did: string): Promise<(Work & { rkey: string })[]> { 173 const response = await this.agent.com.atproto.repo.listRecords({ 174 repo: did, 175 collection: `${LEXICON_PREFIX}.work`, 176 }); 177 return response.data.records.map((r) => ({ 178 ...(r.value as unknown as Work), 179 rkey: r.uri.split('/').pop() || '', 180 })); 181 } 182 183 async createWork(work: Omit<Work, 'createdAt'>): Promise<string> { 184 const rkey = TID.nextStr(); 185 await this.agent.com.atproto.repo.putRecord({ 186 repo: this.agent.session?.did || '', 187 collection: `${LEXICON_PREFIX}.work`, 188 rkey, 189 record: { 190 $type: `${LEXICON_PREFIX}.work`, 191 ...work, 192 createdAt: new Date().toISOString(), 193 }, 194 }); 195 return rkey; 196 } 197 198 async updateWork(rkey: string, updates: Partial<Work>) { 199 const record = await this.agent.com.atproto.repo.getRecord({ 200 repo: this.agent.session?.did || '', 201 collection: `${LEXICON_PREFIX}.work`, 202 rkey, 203 }); 204 205 return this.agent.com.atproto.repo.putRecord({ 206 repo: this.agent.session?.did || '', 207 collection: `${LEXICON_PREFIX}.work`, 208 rkey, 209 record: { 210 ...record.data.value, 211 ...updates, 212 $type: `${LEXICON_PREFIX}.work`, 213 }, 214 }); 215 } 216 217 async deleteWork(rkey: string) { 218 return this.agent.com.atproto.repo.deleteRecord({ 219 repo: this.agent.session?.did || '', 220 collection: `${LEXICON_PREFIX}.work`, 221 rkey, 222 }); 223 } 224 225 // Event operations 226 async listEvents(did: string): Promise<Event[]> { 227 const response = await this.agent.com.atproto.repo.listRecords({ 228 repo: did, 229 collection: `${LEXICON_PREFIX}.event`, 230 }); 231 return response.data.records.map((r) => r.value as unknown as Event); 232 } 233 234 async createEvent(event: Omit<Event, 'createdAt'>): Promise<string> { 235 const rkey = TID.nextStr(); 236 await this.agent.com.atproto.repo.putRecord({ 237 repo: this.agent.session?.did || '', 238 collection: `${LEXICON_PREFIX}.event`, 239 rkey, 240 record: { 241 $type: `${LEXICON_PREFIX}.event`, 242 ...event, 243 createdAt: new Date().toISOString(), 244 }, 245 }); 246 return rkey; 247 } 248 249 async updateEvent(rkey: string, updates: Partial<Event>) { 250 const record = await this.agent.com.atproto.repo.getRecord({ 251 repo: this.agent.session?.did || '', 252 collection: `${LEXICON_PREFIX}.event`, 253 rkey, 254 }); 255 256 return this.agent.com.atproto.repo.putRecord({ 257 repo: this.agent.session?.did || '', 258 collection: `${LEXICON_PREFIX}.event`, 259 rkey, 260 record: { 261 ...record.data.value, 262 ...updates, 263 $type: `${LEXICON_PREFIX}.event`, 264 }, 265 }); 266 } 267 268 async deleteEvent(rkey: string) { 269 return this.agent.com.atproto.repo.deleteRecord({ 270 repo: this.agent.session?.did || '', 271 collection: `${LEXICON_PREFIX}.event`, 272 rkey, 273 }); 274 } 275}