forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview/models: move db.Issue* into models

- move db.{Issue,IssueComment} into models
- move auxilliary funcs like CommentTree into models

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 6a7d161d dc802605

verified
Changed files
+249 -238
appview
+15 -200
appview/db/issues.go
···
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
-
"tangled.org/core/api/tangled"
"tangled.org/core/appview/models"
"tangled.org/core/appview/pagination"
)
-
type Issue struct {
-
Id int64
-
Did string
-
Rkey string
-
RepoAt syntax.ATURI
-
IssueId int
-
Created time.Time
-
Edited *time.Time
-
Deleted *time.Time
-
Title string
-
Body string
-
Open bool
-
-
// optionally, populate this when querying for reverse mappings
-
// like comment counts, parent repo etc.
-
Comments []IssueComment
-
Labels models.LabelState
-
Repo *models.Repo
-
}
-
-
func (i *Issue) AtUri() syntax.ATURI {
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey))
-
}
-
-
func (i *Issue) AsRecord() tangled.RepoIssue {
-
return tangled.RepoIssue{
-
Repo: i.RepoAt.String(),
-
Title: i.Title,
-
Body: &i.Body,
-
CreatedAt: i.Created.Format(time.RFC3339),
-
}
-
}
-
-
func (i *Issue) State() string {
-
if i.Open {
-
return "open"
-
}
-
return "closed"
-
}
-
-
type CommentListItem struct {
-
Self *IssueComment
-
Replies []*IssueComment
-
}
-
-
func (i *Issue) CommentList() []CommentListItem {
-
// Create a map to quickly find comments by their aturi
-
toplevel := make(map[string]*CommentListItem)
-
var replies []*IssueComment
-
-
// collect top level comments into the map
-
for _, comment := range i.Comments {
-
if comment.IsTopLevel() {
-
toplevel[comment.AtUri().String()] = &CommentListItem{
-
Self: &comment,
-
}
-
} else {
-
replies = append(replies, &comment)
-
}
-
}
-
-
for _, r := range replies {
-
parentAt := *r.ReplyTo
-
if parent, exists := toplevel[parentAt]; exists {
-
parent.Replies = append(parent.Replies, r)
-
}
-
}
-
-
var listing []CommentListItem
-
for _, v := range toplevel {
-
listing = append(listing, *v)
-
}
-
-
// sort everything
-
sortFunc := func(a, b *IssueComment) bool {
-
return a.Created.Before(b.Created)
-
}
-
sort.Slice(listing, func(i, j int) bool {
-
return sortFunc(listing[i].Self, listing[j].Self)
-
})
-
for _, r := range listing {
-
sort.Slice(r.Replies, func(i, j int) bool {
-
return sortFunc(r.Replies[i], r.Replies[j])
-
})
-
}
-
-
return listing
-
}
-
-
func (i *Issue) Participants() []string {
-
participantSet := make(map[string]struct{})
-
participants := []string{}
-
-
addParticipant := func(did string) {
-
if _, exists := participantSet[did]; !exists {
-
participantSet[did] = struct{}{}
-
participants = append(participants, did)
-
}
-
}
-
-
addParticipant(i.Did)
-
-
for _, c := range i.Comments {
-
addParticipant(c.Did)
-
}
-
-
return participants
-
}
-
-
func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue {
-
created, err := time.Parse(time.RFC3339, record.CreatedAt)
-
if err != nil {
-
created = time.Now()
-
}
-
-
body := ""
-
if record.Body != nil {
-
body = *record.Body
-
}
-
-
return Issue{
-
RepoAt: syntax.ATURI(record.Repo),
-
Did: did,
-
Rkey: rkey,
-
Created: created,
-
Title: record.Title,
-
Body: body,
-
Open: true, // new issues are open by default
-
}
-
}
-
-
type IssueComment struct {
-
Id int64
-
Did string
-
Rkey string
-
IssueAt string
-
ReplyTo *string
-
Body string
-
Created time.Time
-
Edited *time.Time
-
Deleted *time.Time
-
}
-
-
func (i *IssueComment) AtUri() syntax.ATURI {
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))
-
}
-
-
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
-
return tangled.RepoIssueComment{
-
Body: i.Body,
-
Issue: i.IssueAt,
-
CreatedAt: i.Created.Format(time.RFC3339),
-
ReplyTo: i.ReplyTo,
-
}
-
}
-
-
func (i *IssueComment) IsTopLevel() bool {
-
return i.ReplyTo == nil
-
}
-
-
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
-
created, err := time.Parse(time.RFC3339, record.CreatedAt)
-
if err != nil {
-
created = time.Now()
-
}
-
-
ownerDid := did
-
-
if _, err = syntax.ParseATURI(record.Issue); err != nil {
-
return nil, err
-
}
-
-
comment := IssueComment{
-
Did: ownerDid,
-
Rkey: rkey,
-
Body: record.Body,
-
IssueAt: record.Issue,
-
ReplyTo: record.ReplyTo,
-
Created: created,
-
}
-
-
return &comment, nil
-
}
-
-
func PutIssue(tx *sql.Tx, issue *Issue) error {
+
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
// ensure sequence exists
_, err := tx.Exec(`
insert or ignore into repo_issue_seqs (repo_at, next_issue_id)
···
}
}
-
func createNewIssue(tx *sql.Tx, issue *Issue) error {
+
func createNewIssue(tx *sql.Tx, issue *models.Issue) error {
// get next issue_id
var newIssueId int
err := tx.QueryRow(`
···
return row.Scan(&issue.Id, &issue.IssueId)
}
-
func updateIssue(tx *sql.Tx, issue *Issue) error {
+
func updateIssue(tx *sql.Tx, issue *models.Issue) error {
// update existing issue
_, err := tx.Exec(`
update issues
···
return err
}
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]Issue, error) {
-
issueMap := make(map[string]*Issue) // at-uri -> issue
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
+
issueMap := make(map[string]*models.Issue) // at-uri -> issue
var conditions []string
var args []any
···
defer rows.Close()
for rows.Next() {
-
var issue Issue
+
var issue models.Issue
var createdAt string
var editedAt, deletedAt sql.Null[string]
var rowNum int64
···
}
}
-
var issues []Issue
+
var issues []models.Issue
for _, i := range issueMap {
issues = append(issues, *i)
}
···
return issues, nil
}
-
func GetIssues(e Execer, filters ...filter) ([]Issue, error) {
+
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
}
-
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
+
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*models.Issue, error) {
query := `select id, owner_did, rkey, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
-
var issue Issue
+
var issue models.Issue
var createdAt string
err := row.Scan(&issue.Id, &issue.Did, &issue.Rkey, &createdAt, &issue.Title, &issue.Body, &issue.Open)
if err != nil {
···
return &issue, nil
}
-
func AddIssueComment(e Execer, c IssueComment) (int64, error) {
+
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
result, err := e.Exec(
`insert into issue_comments (
did,
···
return err
}
-
func GetIssueComments(e Execer, filters ...filter) ([]IssueComment, error) {
-
var comments []IssueComment
+
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
+
var comments []models.IssueComment
var conditions []string
var args []any
···
}
for rows.Next() {
-
var comment IssueComment
+
var comment models.IssueComment
var created string
var rkey, edited, deleted, replyTo sql.Null[string]
err := rows.Scan(
···
var count models.IssueCount
if err := row.Scan(&count.Open, &count.Closed); err != nil {
-
return models.IssueCount{0, 0}, err
+
return models.IssueCount{}, err
}
return count, nil
+1 -1
appview/db/profile.go
···
}
type IssueEvents struct {
-
Items []*Issue
+
Items []*models.Issue
}
type IssueEventStats struct {
+2 -2
appview/ingester.go
···
return err
}
-
issue := db.IssueFromRecord(did, rkey, record)
+
issue := models.IssueFromRecord(did, rkey, record)
if err := i.Validator.ValidateIssue(&issue); err != nil {
return fmt.Errorf("failed to validate issue: %w", err)
···
return fmt.Errorf("invalid record: %w", err)
}
-
comment, err := db.IssueCommentFromRecord(did, rkey, record)
+
comment, err := models.IssueCommentFromRecord(did, rkey, record)
if err != nil {
return fmt.Errorf("failed to parse comment from record: %w", err)
}
+13 -13
appview/issues/issues.go
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Notice(w, noticeId, "Failed to delete issue.")
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
replyTo = &replyToUri
}
-
comment := db.IssueComment{
+
comment := models.IssueComment{
Did: user.Did,
Rkey: tid.TID(),
IssueAt: issue.AtUri().String(),
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
return
}
-
issue, ok := r.Context().Value("issue").(*db.Issue)
+
issue, ok := r.Context().Value("issue").(*models.Issue)
if !ok {
l.Error("failed to get issue")
rp.pages.Error404(w)
···
RepoInfo: f.RepoInfo(user),
})
case http.MethodPost:
-
issue := &db.Issue{
+
issue := &models.Issue{
RepoAt: f.RepoAt(),
Rkey: tid.TID(),
Title: r.FormValue("title"),
+194
appview/models/issue.go
···
+
package models
+
+
import (
+
"fmt"
+
"sort"
+
"time"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"tangled.org/core/api/tangled"
+
)
+
+
type Issue struct {
+
Id int64
+
Did string
+
Rkey string
+
RepoAt syntax.ATURI
+
IssueId int
+
Created time.Time
+
Edited *time.Time
+
Deleted *time.Time
+
Title string
+
Body string
+
Open bool
+
+
// optionally, populate this when querying for reverse mappings
+
// like comment counts, parent repo etc.
+
Comments []IssueComment
+
Labels LabelState
+
Repo *Repo
+
}
+
+
func (i *Issue) AtUri() syntax.ATURI {
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey))
+
}
+
+
func (i *Issue) AsRecord() tangled.RepoIssue {
+
return tangled.RepoIssue{
+
Repo: i.RepoAt.String(),
+
Title: i.Title,
+
Body: &i.Body,
+
CreatedAt: i.Created.Format(time.RFC3339),
+
}
+
}
+
+
func (i *Issue) State() string {
+
if i.Open {
+
return "open"
+
}
+
return "closed"
+
}
+
+
type CommentListItem struct {
+
Self *IssueComment
+
Replies []*IssueComment
+
}
+
+
func (i *Issue) CommentList() []CommentListItem {
+
// Create a map to quickly find comments by their aturi
+
toplevel := make(map[string]*CommentListItem)
+
var replies []*IssueComment
+
+
// collect top level comments into the map
+
for _, comment := range i.Comments {
+
if comment.IsTopLevel() {
+
toplevel[comment.AtUri().String()] = &CommentListItem{
+
Self: &comment,
+
}
+
} else {
+
replies = append(replies, &comment)
+
}
+
}
+
+
for _, r := range replies {
+
parentAt := *r.ReplyTo
+
if parent, exists := toplevel[parentAt]; exists {
+
parent.Replies = append(parent.Replies, r)
+
}
+
}
+
+
var listing []CommentListItem
+
for _, v := range toplevel {
+
listing = append(listing, *v)
+
}
+
+
// sort everything
+
sortFunc := func(a, b *IssueComment) bool {
+
return a.Created.Before(b.Created)
+
}
+
sort.Slice(listing, func(i, j int) bool {
+
return sortFunc(listing[i].Self, listing[j].Self)
+
})
+
for _, r := range listing {
+
sort.Slice(r.Replies, func(i, j int) bool {
+
return sortFunc(r.Replies[i], r.Replies[j])
+
})
+
}
+
+
return listing
+
}
+
+
func (i *Issue) Participants() []string {
+
participantSet := make(map[string]struct{})
+
participants := []string{}
+
+
addParticipant := func(did string) {
+
if _, exists := participantSet[did]; !exists {
+
participantSet[did] = struct{}{}
+
participants = append(participants, did)
+
}
+
}
+
+
addParticipant(i.Did)
+
+
for _, c := range i.Comments {
+
addParticipant(c.Did)
+
}
+
+
return participants
+
}
+
+
func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue {
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
+
if err != nil {
+
created = time.Now()
+
}
+
+
body := ""
+
if record.Body != nil {
+
body = *record.Body
+
}
+
+
return Issue{
+
RepoAt: syntax.ATURI(record.Repo),
+
Did: did,
+
Rkey: rkey,
+
Created: created,
+
Title: record.Title,
+
Body: body,
+
Open: true, // new issues are open by default
+
}
+
}
+
+
type IssueComment struct {
+
Id int64
+
Did string
+
Rkey string
+
IssueAt string
+
ReplyTo *string
+
Body string
+
Created time.Time
+
Edited *time.Time
+
Deleted *time.Time
+
}
+
+
func (i *IssueComment) AtUri() syntax.ATURI {
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))
+
}
+
+
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
+
return tangled.RepoIssueComment{
+
Body: i.Body,
+
Issue: i.IssueAt,
+
CreatedAt: i.Created.Format(time.RFC3339),
+
ReplyTo: i.ReplyTo,
+
}
+
}
+
+
func (i *IssueComment) IsTopLevel() bool {
+
return i.ReplyTo == nil
+
}
+
+
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
+
if err != nil {
+
created = time.Now()
+
}
+
+
ownerDid := did
+
+
if _, err = syntax.ParseATURI(record.Issue); err != nil {
+
return nil, err
+
}
+
+
comment := IssueComment{
+
Did: ownerDid,
+
Rkey: rkey,
+
Body: record.Body,
+
IssueAt: record.Issue,
+
ReplyTo: record.ReplyTo,
+
Created: created,
+
}
+
+
return &comment, nil
+
}
+1 -1
appview/notify/merged_notifier.go
···
}
}
-
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *db.Issue) {
+
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
for _, notifier := range m.notifiers {
notifier.NewIssue(ctx, issue)
}
+2 -2
appview/notify/notifier.go
···
NewStar(ctx context.Context, star *db.Star)
DeleteStar(ctx context.Context, star *db.Star)
-
NewIssue(ctx context.Context, issue *db.Issue)
+
NewIssue(ctx context.Context, issue *models.Issue)
NewFollow(ctx context.Context, follow *models.Follow)
DeleteFollow(ctx context.Context, follow *models.Follow)
···
func (m *BaseNotifier) NewStar(ctx context.Context, star *db.Star) {}
func (m *BaseNotifier) DeleteStar(ctx context.Context, star *db.Star) {}
-
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) {}
+
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {}
func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {}
func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {}
+13 -13
appview/pages/pages.go
···
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
Active string
-
Issues []db.Issue
+
Issues []models.Issue
LabelDefs map[string]*models.LabelDefinition
Page pagination.Page
FilteringByOpen bool
···
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
Active string
-
Issue *db.Issue
-
CommentList []db.CommentListItem
+
Issue *models.Issue
+
CommentList []models.CommentListItem
LabelDefs map[string]*models.LabelDefinition
OrderedReactionKinds []db.ReactionKind
···
type EditIssueParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Issue *db.Issue
+
Issue *models.Issue
Action string
}
···
type RepoNewIssueParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Issue *db.Issue // existing issue if any -- passed when editing
+
Issue *models.Issue // existing issue if any -- passed when editing
Active string
Action string
}
···
type EditIssueCommentParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Issue *db.Issue
-
Comment *db.IssueComment
+
Issue *models.Issue
+
Comment *models.IssueComment
}
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
type ReplyIssueCommentPlaceholderParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Issue *db.Issue
-
Comment *db.IssueComment
+
Issue *models.Issue
+
Comment *models.IssueComment
}
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
type ReplyIssueCommentParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Issue *db.Issue
-
Comment *db.IssueComment
+
Issue *models.Issue
+
Comment *models.IssueComment
}
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
type IssueCommentBodyParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
-
Issue *db.Issue
-
Comment *db.IssueComment
+
Issue *models.Issue
+
Comment *models.IssueComment
}
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+1 -1
appview/posthog/notifier.go
···
}
}
-
func (n *posthogNotifier) NewIssue(ctx context.Context, issue *db.Issue) {
+
func (n *posthogNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
err := n.client.Enqueue(posthog.Capture{
DistinctId: issue.Did,
Event: "new_issue",
+2 -1
appview/repo/feed.go
···
"time"
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
"tangled.org/core/appview/pagination"
"tangled.org/core/appview/reporesolver"
···
return items, nil
}
-
func (rp *Repo) createIssueItem(ctx context.Context, issue db.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) {
+
func (rp *Repo) createIssueItem(ctx context.Context, issue models.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) {
owner, err := rp.idResolver.ResolveIdent(ctx, issue.Did)
if err != nil {
return nil, err
+2 -2
appview/state/profile.go
···
return nil
}
-
func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*db.Issue, author *feeds.Author) error {
+
func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*models.Issue, author *feeds.Author) error {
for _, issue := range issues {
owner, err := s.idResolver.ResolveIdent(ctx, issue.Repo.Did)
if err != nil {
···
}
}
-
func (s *State) createIssueItem(issue *db.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item {
+
func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item {
return &feeds.Item{
Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name),
Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"},
+3 -2
appview/validator/issue.go
···
"strings"
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
)
-
func (v *Validator) ValidateIssueComment(comment *db.IssueComment) error {
+
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
// if comments have parents, only ingest ones that are 1 level deep
if comment.ReplyTo != nil {
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
···
return nil
}
-
func (v *Validator) ValidateIssue(issue *db.Issue) error {
+
func (v *Validator) ValidateIssue(issue *models.Issue) error {
if issue.Title == "" {
return fmt.Errorf("issue title is empty")
}