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