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