Lanyards is a dedicated profile for researchers, built on the AT Protocol.
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}