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