1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 securejoin "github.com/cyphar/filepath-securejoin"
10 "tangled.sh/tangled.sh/core/api/tangled"
11)
12
13type Repo struct {
14 Did string
15 Name string
16 Knot string
17 Rkey string
18 Created time.Time
19 AtUri string
20 Description string
21
22 // optionally, populate this when querying for reverse mappings
23 RepoStats *RepoStats
24
25 // optional
26 Source string
27}
28
29func (r Repo) RepoAt() syntax.ATURI {
30 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
31}
32
33func (r Repo) DidSlashRepo() string {
34 p, _ := securejoin.SecureJoin(r.Did, r.Name)
35 return p
36}
37
38func GetAllRepos(e Execer, limit int) ([]Repo, error) {
39 var repos []Repo
40
41 rows, err := e.Query(
42 `select did, name, knot, rkey, description, created, source
43 from repos
44 order by created desc
45 limit ?
46 `,
47 limit,
48 )
49 if err != nil {
50 return nil, err
51 }
52 defer rows.Close()
53
54 for rows.Next() {
55 var repo Repo
56 err := scanRepo(
57 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source,
58 )
59 if err != nil {
60 return nil, err
61 }
62 repos = append(repos, repo)
63 }
64
65 if err := rows.Err(); err != nil {
66 return nil, err
67 }
68
69 return repos, nil
70}
71
72func GetAllReposByDid(e Execer, did string) ([]Repo, error) {
73 var repos []Repo
74
75 rows, err := e.Query(
76 `select
77 r.did,
78 r.name,
79 r.knot,
80 r.rkey,
81 r.description,
82 r.created,
83 count(s.id) as star_count,
84 r.source
85 from
86 repos r
87 left join
88 stars s on r.at_uri = s.repo_at
89 where
90 r.did = ?
91 group by
92 r.at_uri
93 order by r.created desc`,
94 did)
95 if err != nil {
96 return nil, err
97 }
98 defer rows.Close()
99
100 for rows.Next() {
101 var repo Repo
102 var repoStats RepoStats
103 var createdAt string
104 var nullableDescription sql.NullString
105 var nullableSource sql.NullString
106
107 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource)
108 if err != nil {
109 return nil, err
110 }
111
112 if nullableDescription.Valid {
113 repo.Description = nullableDescription.String
114 }
115
116 if nullableSource.Valid {
117 repo.Source = nullableSource.String
118 }
119
120 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
121 if err != nil {
122 repo.Created = time.Now()
123 } else {
124 repo.Created = createdAtTime
125 }
126
127 repo.RepoStats = &repoStats
128
129 repos = append(repos, repo)
130 }
131
132 if err := rows.Err(); err != nil {
133 return nil, err
134 }
135
136 return repos, nil
137}
138
139func GetRepo(e Execer, did, name string) (*Repo, error) {
140 var repo Repo
141 var nullableDescription sql.NullString
142
143 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name)
144
145 var createdAt string
146 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
147 return nil, err
148 }
149 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
150 repo.Created = createdAtTime
151
152 if nullableDescription.Valid {
153 repo.Description = nullableDescription.String
154 } else {
155 repo.Description = ""
156 }
157
158 return &repo, nil
159}
160
161func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) {
162 var repo Repo
163 var nullableDescription sql.NullString
164
165 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri)
166
167 var createdAt string
168 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
169 return nil, err
170 }
171 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
172 repo.Created = createdAtTime
173
174 if nullableDescription.Valid {
175 repo.Description = nullableDescription.String
176 } else {
177 repo.Description = ""
178 }
179
180 return &repo, nil
181}
182
183func AddRepo(e Execer, repo *Repo) error {
184 _, err := e.Exec(
185 `insert into repos
186 (did, name, knot, rkey, at_uri, description, source)
187 values (?, ?, ?, ?, ?, ?, ?)`,
188 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source,
189 )
190 return err
191}
192
193func RemoveRepo(e Execer, did, name string) error {
194 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
195 return err
196}
197
198func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
199 var nullableSource sql.NullString
200 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
201 if err != nil {
202 return "", err
203 }
204 return nullableSource.String, nil
205}
206
207func GetForksByDid(e Execer, did string) ([]Repo, error) {
208 var repos []Repo
209
210 rows, err := e.Query(
211 `select did, name, knot, rkey, description, created, at_uri, source
212 from repos
213 where did = ? and source is not null and source != ''
214 order by created desc`,
215 did,
216 )
217 if err != nil {
218 return nil, err
219 }
220 defer rows.Close()
221
222 for rows.Next() {
223 var repo Repo
224 var createdAt string
225 var nullableDescription sql.NullString
226 var nullableSource sql.NullString
227
228 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
229 if err != nil {
230 return nil, err
231 }
232
233 if nullableDescription.Valid {
234 repo.Description = nullableDescription.String
235 }
236
237 if nullableSource.Valid {
238 repo.Source = nullableSource.String
239 }
240
241 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
242 if err != nil {
243 repo.Created = time.Now()
244 } else {
245 repo.Created = createdAtTime
246 }
247
248 repos = append(repos, repo)
249 }
250
251 if err := rows.Err(); err != nil {
252 return nil, err
253 }
254
255 return repos, nil
256}
257
258func GetForkByDid(e Execer, did string, name string) (*Repo, error) {
259 var repo Repo
260 var createdAt string
261 var nullableDescription sql.NullString
262 var nullableSource sql.NullString
263
264 row := e.QueryRow(
265 `select did, name, knot, rkey, description, created, at_uri, source
266 from repos
267 where did = ? and name = ? and source is not null and source != ''`,
268 did, name,
269 )
270
271 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
272 if err != nil {
273 return nil, err
274 }
275
276 if nullableDescription.Valid {
277 repo.Description = nullableDescription.String
278 }
279
280 if nullableSource.Valid {
281 repo.Source = nullableSource.String
282 }
283
284 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
285 if err != nil {
286 repo.Created = time.Now()
287 } else {
288 repo.Created = createdAtTime
289 }
290
291 return &repo, nil
292}
293
294func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
295 _, err := e.Exec(
296 `insert into collaborators (did, repo)
297 values (?, (select id from repos where did = ? and name = ? and knot = ?));`,
298 collaborator, repoOwnerDid, repoName, repoKnot)
299 return err
300}
301
302func UpdateDescription(e Execer, repoAt, newDescription string) error {
303 _, err := e.Exec(
304 `update repos set description = ? where at_uri = ?`, newDescription, repoAt)
305 return err
306}
307
308func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) {
309 var repos []Repo
310
311 rows, err := e.Query(
312 `select
313 r.did, r.name, r.knot, r.rkey, r.description, r.created, count(s.id) as star_count
314 from
315 repos r
316 join
317 collaborators c on r.id = c.repo
318 left join
319 stars s on r.at_uri = s.repo_at
320 where
321 c.did = ?
322 group by
323 r.id;`, collaborator)
324 if err != nil {
325 return nil, err
326 }
327 defer rows.Close()
328
329 for rows.Next() {
330 var repo Repo
331 var repoStats RepoStats
332 var createdAt string
333 var nullableDescription sql.NullString
334
335 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount)
336 if err != nil {
337 return nil, err
338 }
339
340 if nullableDescription.Valid {
341 repo.Description = nullableDescription.String
342 } else {
343 repo.Description = ""
344 }
345
346 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
347 if err != nil {
348 repo.Created = time.Now()
349 } else {
350 repo.Created = createdAtTime
351 }
352
353 repo.RepoStats = &repoStats
354
355 repos = append(repos, repo)
356 }
357
358 if err := rows.Err(); err != nil {
359 return nil, err
360 }
361
362 return repos, nil
363}
364
365type RepoStats struct {
366 StarCount int
367 IssueCount IssueCount
368 PullCount PullCount
369}
370
371func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error {
372 var createdAt string
373 var nullableDescription sql.NullString
374 var nullableSource sql.NullString
375 if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil {
376 return err
377 }
378
379 if nullableDescription.Valid {
380 *description = nullableDescription.String
381 } else {
382 *description = ""
383 }
384
385 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
386 if err != nil {
387 *created = time.Now()
388 } else {
389 *created = createdAtTime
390 }
391
392 if nullableSource.Valid {
393 *source = nullableSource.String
394 } else {
395 *source = ""
396 }
397
398 return nil
399}