forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db
2
3import (
4 "context"
5 "database/sql"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "go.opentelemetry.io/otel"
10 "go.opentelemetry.io/otel/attribute"
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 GetAllRepos(ctx context.Context, e Execer, limit int) ([]Repo, error) {
30 ctx, span := otel.Tracer("db").Start(ctx, "GetAllRepos")
31 defer span.End()
32 span.SetAttributes(attribute.Int("limit", limit))
33
34 var repos []Repo
35
36 rows, err := e.Query(
37 `select did, name, knot, rkey, description, created, source
38 from repos
39 order by created desc
40 limit ?
41 `,
42 limit,
43 )
44 if err != nil {
45 span.RecordError(err)
46 return nil, err
47 }
48 defer rows.Close()
49
50 for rows.Next() {
51 var repo Repo
52 err := scanRepo(
53 rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source,
54 )
55 if err != nil {
56 span.RecordError(err)
57 return nil, err
58 }
59 repos = append(repos, repo)
60 }
61
62 if err := rows.Err(); err != nil {
63 span.RecordError(err)
64 return nil, err
65 }
66
67 span.SetAttributes(attribute.Int("repos.count", len(repos)))
68 return repos, nil
69}
70
71func GetAllReposByDid(ctx context.Context, e Execer, did string) ([]Repo, error) {
72 ctx, span := otel.Tracer("db").Start(ctx, "GetAllReposByDid")
73 defer span.End()
74 span.SetAttributes(attribute.String("did", did))
75
76 var repos []Repo
77
78 rows, err := e.Query(
79 `select
80 r.did,
81 r.name,
82 r.knot,
83 r.rkey,
84 r.description,
85 r.created,
86 count(s.id) as star_count,
87 r.source
88 from
89 repos r
90 left join
91 stars s on r.at_uri = s.repo_at
92 where
93 r.did = ?
94 group by
95 r.at_uri
96 order by r.created desc`,
97 did)
98 if err != nil {
99 span.RecordError(err)
100 return nil, err
101 }
102 defer rows.Close()
103
104 for rows.Next() {
105 var repo Repo
106 var repoStats RepoStats
107 var createdAt string
108 var nullableDescription sql.NullString
109 var nullableSource sql.NullString
110
111 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource)
112 if err != nil {
113 span.RecordError(err)
114 return nil, err
115 }
116
117 if nullableDescription.Valid {
118 repo.Description = nullableDescription.String
119 }
120
121 if nullableSource.Valid {
122 repo.Source = nullableSource.String
123 }
124
125 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
126 if err != nil {
127 repo.Created = time.Now()
128 } else {
129 repo.Created = createdAtTime
130 }
131
132 repo.RepoStats = &repoStats
133
134 repos = append(repos, repo)
135 }
136
137 if err := rows.Err(); err != nil {
138 span.RecordError(err)
139 return nil, err
140 }
141
142 span.SetAttributes(attribute.Int("repos.count", len(repos)))
143 return repos, nil
144}
145
146func GetRepo(ctx context.Context, e Execer, did, name string) (*Repo, error) {
147 ctx, span := otel.Tracer("db").Start(ctx, "GetRepo")
148 defer span.End()
149 span.SetAttributes(
150 attribute.String("did", did),
151 attribute.String("name", name),
152 )
153
154 var repo Repo
155 var nullableDescription sql.NullString
156
157 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where did = ? and name = ?`, did, name)
158
159 var createdAt string
160 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
161 span.RecordError(err)
162 return nil, err
163 }
164 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
165 repo.Created = createdAtTime
166
167 if nullableDescription.Valid {
168 repo.Description = nullableDescription.String
169 } else {
170 repo.Description = ""
171 }
172
173 return &repo, nil
174}
175
176func GetRepoByAtUri(ctx context.Context, e Execer, atUri string) (*Repo, error) {
177 ctx, span := otel.Tracer("db").Start(ctx, "GetRepoByAtUri")
178 defer span.End()
179 span.SetAttributes(attribute.String("atUri", atUri))
180
181 var repo Repo
182 var nullableDescription sql.NullString
183
184 row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri)
185
186 var createdAt string
187 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
188 span.RecordError(err)
189 return nil, err
190 }
191 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
192 repo.Created = createdAtTime
193
194 if nullableDescription.Valid {
195 repo.Description = nullableDescription.String
196 } else {
197 repo.Description = ""
198 }
199
200 return &repo, nil
201}
202
203func AddRepo(ctx context.Context, e Execer, repo *Repo) error {
204 ctx, span := otel.Tracer("db").Start(ctx, "AddRepo")
205 defer span.End()
206 span.SetAttributes(
207 attribute.String("did", repo.Did),
208 attribute.String("name", repo.Name),
209 )
210
211 _, err := e.Exec(
212 `insert into repos
213 (did, name, knot, rkey, at_uri, description, source)
214 values (?, ?, ?, ?, ?, ?, ?)`,
215 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source,
216 )
217 if err != nil {
218 span.RecordError(err)
219 }
220 return err
221}
222
223func RemoveRepo(ctx context.Context, e Execer, did, name string) error {
224 ctx, span := otel.Tracer("db").Start(ctx, "RemoveRepo")
225 defer span.End()
226 span.SetAttributes(
227 attribute.String("did", did),
228 attribute.String("name", name),
229 )
230
231 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
232 if err != nil {
233 span.RecordError(err)
234 }
235 return err
236}
237
238func GetRepoSource(ctx context.Context, e Execer, repoAt syntax.ATURI) (string, error) {
239 ctx, span := otel.Tracer("db").Start(ctx, "GetRepoSource")
240 defer span.End()
241 span.SetAttributes(attribute.String("repoAt", repoAt.String()))
242
243 var nullableSource sql.NullString
244 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
245 if err != nil {
246 span.RecordError(err)
247 return "", err
248 }
249 return nullableSource.String, nil
250}
251
252func GetForksByDid(ctx context.Context, e Execer, did string) ([]Repo, error) {
253 ctx, span := otel.Tracer("db").Start(ctx, "GetForksByDid")
254 defer span.End()
255 span.SetAttributes(attribute.String("did", did))
256
257 var repos []Repo
258
259 rows, err := e.Query(
260 `select did, name, knot, rkey, description, created, at_uri, source
261 from repos
262 where did = ? and source is not null and source != ''
263 order by created desc`,
264 did,
265 )
266 if err != nil {
267 span.RecordError(err)
268 return nil, err
269 }
270 defer rows.Close()
271
272 for rows.Next() {
273 var repo Repo
274 var createdAt string
275 var nullableDescription sql.NullString
276 var nullableSource sql.NullString
277
278 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
279 if err != nil {
280 span.RecordError(err)
281 return nil, err
282 }
283
284 if nullableDescription.Valid {
285 repo.Description = nullableDescription.String
286 }
287
288 if nullableSource.Valid {
289 repo.Source = nullableSource.String
290 }
291
292 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
293 if err != nil {
294 repo.Created = time.Now()
295 } else {
296 repo.Created = createdAtTime
297 }
298
299 repos = append(repos, repo)
300 }
301
302 if err := rows.Err(); err != nil {
303 span.RecordError(err)
304 return nil, err
305 }
306
307 span.SetAttributes(attribute.Int("forks.count", len(repos)))
308 return repos, nil
309}
310
311func GetForkByDid(ctx context.Context, e Execer, did string, name string) (*Repo, error) {
312 ctx, span := otel.Tracer("db").Start(ctx, "GetForkByDid")
313 defer span.End()
314 span.SetAttributes(
315 attribute.String("did", did),
316 attribute.String("name", name),
317 )
318
319 var repo Repo
320 var createdAt string
321 var nullableDescription sql.NullString
322 var nullableSource sql.NullString
323
324 row := e.QueryRow(
325 `select did, name, knot, rkey, description, created, at_uri, source
326 from repos
327 where did = ? and name = ? and source is not null and source != ''`,
328 did, name,
329 )
330
331 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
332 if err != nil {
333 span.RecordError(err)
334 return nil, err
335 }
336
337 if nullableDescription.Valid {
338 repo.Description = nullableDescription.String
339 }
340
341 if nullableSource.Valid {
342 repo.Source = nullableSource.String
343 }
344
345 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
346 if err != nil {
347 repo.Created = time.Now()
348 } else {
349 repo.Created = createdAtTime
350 }
351
352 return &repo, nil
353}
354
355func AddCollaborator(ctx context.Context, e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
356 ctx, span := otel.Tracer("db").Start(ctx, "AddCollaborator")
357 defer span.End()
358 span.SetAttributes(
359 attribute.String("collaborator", collaborator),
360 attribute.String("repoOwnerDid", repoOwnerDid),
361 attribute.String("repoName", repoName),
362 )
363
364 _, err := e.Exec(
365 `insert into collaborators (did, repo)
366 values (?, (select id from repos where did = ? and name = ? and knot = ?));`,
367 collaborator, repoOwnerDid, repoName, repoKnot)
368 if err != nil {
369 span.RecordError(err)
370 }
371 return err
372}
373
374func UpdateDescription(ctx context.Context, e Execer, repoAt, newDescription string) error {
375 ctx, span := otel.Tracer("db").Start(ctx, "UpdateDescription")
376 defer span.End()
377 span.SetAttributes(
378 attribute.String("repoAt", repoAt),
379 attribute.String("description", newDescription),
380 )
381
382 _, err := e.Exec(
383 `update repos set description = ? where at_uri = ?`, newDescription, repoAt)
384 if err != nil {
385 span.RecordError(err)
386 }
387 return err
388}
389
390func CollaboratingIn(ctx context.Context, e Execer, collaborator string) ([]Repo, error) {
391 ctx, span := otel.Tracer("db").Start(ctx, "CollaboratingIn")
392 defer span.End()
393 span.SetAttributes(attribute.String("collaborator", collaborator))
394
395 var repos []Repo
396
397 rows, err := e.Query(
398 `select
399 r.did, r.name, r.knot, r.rkey, r.description, r.created, count(s.id) as star_count
400 from
401 repos r
402 join
403 collaborators c on r.id = c.repo
404 left join
405 stars s on r.at_uri = s.repo_at
406 where
407 c.did = ?
408 group by
409 r.id;`, collaborator)
410 if err != nil {
411 span.RecordError(err)
412 return nil, err
413 }
414 defer rows.Close()
415
416 for rows.Next() {
417 var repo Repo
418 var repoStats RepoStats
419 var createdAt string
420 var nullableDescription sql.NullString
421
422 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount)
423 if err != nil {
424 span.RecordError(err)
425 return nil, err
426 }
427
428 if nullableDescription.Valid {
429 repo.Description = nullableDescription.String
430 } else {
431 repo.Description = ""
432 }
433
434 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
435 if err != nil {
436 repo.Created = time.Now()
437 } else {
438 repo.Created = createdAtTime
439 }
440
441 repo.RepoStats = &repoStats
442
443 repos = append(repos, repo)
444 }
445
446 if err := rows.Err(); err != nil {
447 span.RecordError(err)
448 return nil, err
449 }
450
451 span.SetAttributes(attribute.Int("repos.count", len(repos)))
452 return repos, nil
453}
454
455type RepoStats struct {
456 StarCount int
457 IssueCount IssueCount
458 PullCount PullCount
459}
460
461func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error {
462 var createdAt string
463 var nullableDescription sql.NullString
464 var nullableSource sql.NullString
465 if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil {
466 return err
467 }
468
469 if nullableDescription.Valid {
470 *description = nullableDescription.String
471 } else {
472 *description = ""
473 }
474
475 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
476 if err != nil {
477 *created = time.Now()
478 } else {
479 *created = createdAtTime
480 }
481
482 if nullableSource.Valid {
483 *source = nullableSource.String
484 } else {
485 *source = ""
486 }
487
488 return nil
489}