hmn/src/hmndata/snippet_helper.go

297 lines
7.0 KiB
Go
Raw Normal View History

package hmndata
2021-11-11 19:00:46 +00:00
import (
"context"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
2021-11-11 19:00:46 +00:00
)
type SnippetQuery struct {
IDs []int
OwnerIDs []int
2022-08-05 04:03:45 +00:00
ProjectIDs []int
Tags []int
DiscordMessageIDs []string
2021-11-11 19:00:46 +00:00
Limit, Offset int // if empty, no pagination
}
type SnippetAndStuff struct {
Snippet models.Snippet
Owner *models.User
Asset *models.Asset `db:"asset"`
DiscordMessage *models.DiscordMessage `db:"discord_message"`
Tags []*models.Tag
2022-08-05 04:03:45 +00:00
Projects []*ProjectAndStuff
2021-11-11 19:00:46 +00:00
}
func FetchSnippets(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
q SnippetQuery,
) ([]SnippetAndStuff, error) {
perf := perf.ExtractPerf(ctx)
2021-11-11 19:00:46 +00:00
perf.StartBlock("SQL", "Fetch snippets")
defer perf.EndBlock()
tx, err := dbConn.Begin(ctx)
if err != nil {
return nil, oops.New(err, "failed to start transaction")
}
defer tx.Rollback(ctx)
2022-08-05 04:03:45 +00:00
var tagSnippetIDs []int
if len(q.Tags) > 0 {
// Get snippet IDs with this tag, then use that in the main query
snippetIDs, err := db.QueryScalar[int](ctx, tx,
`
SELECT DISTINCT snippet_id
FROM
2022-05-07 13:11:05 +00:00
snippet_tag
JOIN tag ON snippet_tag.tag_id = tag.id
WHERE
2022-05-07 13:11:05 +00:00
tag.id = ANY ($1)
`,
q.Tags,
)
if err != nil {
return nil, oops.New(err, "failed to get snippet IDs for tag")
}
2021-12-09 03:50:35 +00:00
// special early-out: no snippets found for these tags at all
if len(snippetIDs) == 0 {
2021-12-09 03:50:35 +00:00
return nil, nil
}
2022-08-05 04:03:45 +00:00
tagSnippetIDs = snippetIDs
}
var projectSnippetIDs []int
if len(q.ProjectIDs) > 0 {
// Get snippet IDs for these projects, then use that in the main query
snippetIDs, err := db.QueryScalar[int](ctx, tx,
`
SELECT DISTINCT snippet_id
FROM
snippet_project
WHERE
project_id = ANY ($1)
`,
q.ProjectIDs,
)
if err != nil {
return nil, oops.New(err, "failed to get snippet IDs for tag")
}
// special early-out: no snippets found for these projects at all
if len(snippetIDs) == 0 {
return nil, nil
}
projectSnippetIDs = snippetIDs
}
2021-11-11 19:00:46 +00:00
var qb db.QueryBuilder
qb.Add(
`
SELECT $columns
FROM
2022-05-07 13:11:05 +00:00
snippet
LEFT JOIN hmn_user AS owner ON snippet.owner_id = owner.id
2022-08-05 04:03:45 +00:00
LEFT JOIN asset AS avatar ON avatar.id = owner.avatar_asset_id
2022-05-07 13:11:05 +00:00
LEFT JOIN asset ON snippet.asset_id = asset.id
LEFT JOIN discord_message ON snippet.discord_message_id = discord_message.id
2021-11-11 19:00:46 +00:00
WHERE
TRUE
`,
)
if len(q.IDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, q.IDs)
}
2022-08-05 04:03:45 +00:00
if len(tagSnippetIDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, tagSnippetIDs)
}
if len(projectSnippetIDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, projectSnippetIDs)
}
2021-11-11 19:00:46 +00:00
if len(q.OwnerIDs) > 0 {
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
}
if len(q.DiscordMessageIDs) > 0 {
qb.Add(`AND snippet.discord_message_id = ANY ($?)`, q.DiscordMessageIDs)
}
2021-11-11 19:00:46 +00:00
if currentUser == nil {
qb.Add(
`AND owner.status = $? -- snippet owner is Approved`,
models.UserStatusApproved,
)
} else if !currentUser.IsStaff {
qb.Add(
`
AND (
owner.status = $? -- snippet owner is Approved
OR owner.id = $? -- current user is the snippet owner
)
`,
models.UserStatusApproved,
currentUser.ID,
)
}
qb.Add(`ORDER BY snippet.when DESC, snippet.id ASC`)
if q.Limit > 0 {
qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset)
}
type resultRow struct {
Snippet models.Snippet `db:"snippet"`
Owner *models.User `db:"owner"`
2022-08-05 04:03:45 +00:00
AvatarAsset *models.Asset `db:"avatar"`
2021-11-11 19:00:46 +00:00
Asset *models.Asset `db:"asset"`
DiscordMessage *models.DiscordMessage `db:"discord_message"`
}
results, err := db.Query[resultRow](ctx, tx, qb.String(), qb.Args()...)
2021-11-11 19:00:46 +00:00
if err != nil {
return nil, oops.New(err, "failed to fetch threads")
}
result := make([]SnippetAndStuff, len(results)) // allocate extra space because why not
snippetIDs := make([]int, len(results))
for i, row := range results {
2022-08-05 04:03:45 +00:00
if results[i].Owner != nil {
results[i].Owner.AvatarAsset = results[i].AvatarAsset
}
result[i] = SnippetAndStuff{
Snippet: row.Snippet,
Owner: row.Owner,
Asset: row.Asset,
DiscordMessage: row.DiscordMessage,
// no tags! tags next
2021-11-11 19:00:46 +00:00
}
snippetIDs[i] = row.Snippet.ID
}
2021-11-11 19:00:46 +00:00
// Fetch tags
type snippetTagRow struct {
2022-05-07 13:11:05 +00:00
SnippetID int `db:"snippet_tag.snippet_id"`
Tag *models.Tag `db:"tag"`
}
snippetTags, err := db.Query[snippetTagRow](ctx, tx,
`
SELECT $columns
FROM
2022-05-07 13:11:05 +00:00
snippet_tag
JOIN tag ON snippet_tag.tag_id = tag.id
WHERE
2022-05-07 13:11:05 +00:00
snippet_tag.snippet_id = ANY($1)
`,
snippetIDs,
)
if err != nil {
return nil, oops.New(err, "failed to fetch tags for snippets")
}
// associate tags with snippets
resultBySnippetId := make(map[int]*SnippetAndStuff)
for i := range result {
resultBySnippetId[result[i].Snippet.ID] = &result[i]
}
for _, snippetTag := range snippetTags {
item := resultBySnippetId[snippetTag.SnippetID]
item.Tags = append(item.Tags, snippetTag.Tag)
2021-11-11 19:00:46 +00:00
}
2022-08-05 04:03:45 +00:00
// Fetch projects
type snippetProjectRow struct {
SnippetID int `db:"snippet_id"`
ProjectID int `db:"project_id"`
}
snippetProjects, err := db.Query[snippetProjectRow](ctx, tx,
`
SELECT $columns
FROM snippet_project
WHERE snippet_id = ANY($1)
`,
snippetIDs,
)
if err != nil {
return nil, oops.New(err, "failed to fetch project ids for snippets")
}
var projectIds []int
for _, sp := range snippetProjects {
projectIds = append(projectIds, sp.ProjectID)
}
projects, err := FetchProjects(ctx, tx, currentUser, ProjectsQuery{ProjectIDs: projectIds})
if err != nil {
return nil, oops.New(err, "failed to fetch projects for snippets")
}
projectMap := make(map[int]*ProjectAndStuff)
for i := range projects {
projectMap[projects[i].Project.ID] = &projects[i]
}
for _, sp := range snippetProjects {
snip, hasResult := resultBySnippetId[sp.SnippetID]
proj, hasProj := projectMap[sp.ProjectID]
if hasResult && hasProj {
snip.Projects = append(snip.Projects, proj)
}
}
2021-11-11 19:00:46 +00:00
err = tx.Commit(ctx)
if err != nil {
return nil, oops.New(err, "failed to commit transaction")
}
return result, nil
}
func FetchSnippet(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
snippetID int,
q SnippetQuery,
) (SnippetAndStuff, error) {
q.IDs = []int{snippetID}
q.Limit = 1
q.Offset = 0
res, err := FetchSnippets(ctx, dbConn, currentUser, q)
if err != nil {
return SnippetAndStuff{}, oops.New(err, "failed to fetch snippet")
}
if len(res) == 0 {
return SnippetAndStuff{}, db.NotFound
}
return res[0], nil
}
func FetchSnippetForDiscordMessage(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
discordMessageID string,
q SnippetQuery,
) (SnippetAndStuff, error) {
q.DiscordMessageIDs = []string{discordMessageID}
q.Limit = 1
q.Offset = 0
res, err := FetchSnippets(ctx, dbConn, currentUser, q)
if err != nil {
return SnippetAndStuff{}, oops.New(err, "failed to fetch snippet for Discord message")
}
if len(res) == 0 {
return SnippetAndStuff{}, db.NotFound
}
return res[0], nil
}