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