Adopt sqlc for typed Postgres queries behind store entities.
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.PHONY: sqlc
|
||||||
|
sqlc:
|
||||||
|
sqlc generate
|
||||||
|
|
||||||
|
.PHONY: sqlc-check
|
||||||
|
sqlc-check:
|
||||||
|
sqlc diff
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- name: UpsertAnswer :exec
|
||||||
|
INSERT INTO answers (question_id, author_id, body, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
ON CONFLICT (question_id) DO UPDATE
|
||||||
|
SET body = excluded.body, author_id = excluded.author_id, updated_at = excluded.updated_at;
|
||||||
|
|
||||||
|
-- name: GetAnswer :one
|
||||||
|
SELECT a.question_id, a.author_id, u.name AS author_name, a.body, a.created_at, a.updated_at
|
||||||
|
FROM answers a
|
||||||
|
JOIN users u ON u.id = a.author_id
|
||||||
|
WHERE a.question_id = $1;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
-- name: CreateQuestion :exec
|
||||||
|
INSERT INTO questions (id, author_id, title, body, city, hunt_date, hidden, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, 0, $7);
|
||||||
|
|
||||||
|
-- name: HideQuestion :exec
|
||||||
|
UPDATE questions
|
||||||
|
SET hidden = 1
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListHunt :many
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE(SUM(v.value), 0)::bigint AS score,
|
||||||
|
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END::bigint AS answered,
|
||||||
|
COALESCE((
|
||||||
|
SELECT votes.value FROM votes
|
||||||
|
WHERE votes.user_id = sqlc.arg(viewer_id) AND votes.question_id = q.id
|
||||||
|
), 0)::bigint AS user_vote
|
||||||
|
FROM questions q
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
LEFT JOIN votes v ON v.question_id = q.id
|
||||||
|
LEFT JOIN answers a ON a.question_id = q.id
|
||||||
|
WHERE q.hunt_date = sqlc.arg(hunt_date) AND q.hidden = 0
|
||||||
|
GROUP BY q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, a.question_id
|
||||||
|
ORDER BY score DESC, q.created_at ASC;
|
||||||
|
|
||||||
|
-- name: GetQuestion :one
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE((SELECT SUM(votes.value) FROM votes WHERE votes.question_id = q.id), 0)::bigint AS score,
|
||||||
|
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END::bigint AS answered,
|
||||||
|
COALESCE((
|
||||||
|
SELECT votes.value FROM votes
|
||||||
|
WHERE votes.user_id = sqlc.arg(viewer_id) AND votes.question_id = q.id
|
||||||
|
), 0)::bigint AS user_vote
|
||||||
|
FROM questions q
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
LEFT JOIN answers a ON a.question_id = q.id
|
||||||
|
WHERE q.id = sqlc.arg(id);
|
||||||
|
|
||||||
|
-- name: ListQuestionsByAuthor :many
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE((SELECT SUM(votes.value) FROM votes WHERE votes.question_id = q.id), 0)::bigint AS score,
|
||||||
|
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END::bigint AS answered,
|
||||||
|
0::bigint AS user_vote
|
||||||
|
FROM questions q
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
LEFT JOIN answers a ON a.question_id = q.id
|
||||||
|
WHERE q.author_id = sqlc.arg(author_id) AND q.hidden = 0
|
||||||
|
ORDER BY q.created_at DESC;
|
||||||
|
|
||||||
|
-- name: ListQuestionsAnsweredBy :many
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE((SELECT SUM(votes.value) FROM votes WHERE votes.question_id = q.id), 0)::bigint AS score,
|
||||||
|
1::bigint AS answered,
|
||||||
|
0::bigint AS user_vote
|
||||||
|
FROM answers ans
|
||||||
|
JOIN questions q ON q.id = ans.question_id
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
WHERE ans.author_id = sqlc.arg(admin_id) AND q.hidden = 0
|
||||||
|
ORDER BY ans.updated_at DESC;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- name: CreateUser :exec
|
||||||
|
INSERT INTO users (id, username, name, password_hash, role, avatar_url, state, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, '', '', $6);
|
||||||
|
|
||||||
|
-- name: GetUserByID :one
|
||||||
|
SELECT id, username, name, role, avatar_url, state, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetUserByUsername :one
|
||||||
|
SELECT id, username, name, role, avatar_url, state, created_at, password_hash
|
||||||
|
FROM users
|
||||||
|
WHERE username = $1;
|
||||||
|
|
||||||
|
-- name: ListUsers :many
|
||||||
|
SELECT id, username, name, role, avatar_url, state, created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at ASC;
|
||||||
|
|
||||||
|
-- name: CountAdmins :one
|
||||||
|
SELECT COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
WHERE role = $1;
|
||||||
|
|
||||||
|
-- name: GetUserRole :one
|
||||||
|
SELECT role
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateUserRole :execresult
|
||||||
|
UPDATE users
|
||||||
|
SET role = $1
|
||||||
|
WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: UpdateUserState :exec
|
||||||
|
UPDATE users
|
||||||
|
SET state = $1
|
||||||
|
WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: UpdateUserStateAndAvatar :exec
|
||||||
|
UPDATE users
|
||||||
|
SET state = $1, avatar_url = $2
|
||||||
|
WHERE id = $3;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- name: GetVote :one
|
||||||
|
SELECT value
|
||||||
|
FROM votes
|
||||||
|
WHERE user_id = $1 AND question_id = $2;
|
||||||
|
|
||||||
|
-- name: DeleteVote :exec
|
||||||
|
DELETE FROM votes
|
||||||
|
WHERE user_id = $1 AND question_id = $2;
|
||||||
|
|
||||||
|
-- name: UpsertVote :exec
|
||||||
|
INSERT INTO votes (user_id, question_id, value)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (user_id, question_id) DO UPDATE
|
||||||
|
SET value = excluded.value;
|
||||||
+19
-13
@@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"plumber/internal/store/sqlc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Answer is an admin reply to a question.
|
// Answer is an admin reply to a question.
|
||||||
@@ -35,23 +37,27 @@ func (a *Answer) Upsert(ctx context.Context) error {
|
|||||||
a.CreatedAt = now
|
a.CreatedAt = now
|
||||||
}
|
}
|
||||||
a.UpdatedAt = now
|
a.UpdatedAt = now
|
||||||
_, err := a.db.ExecContext(ctx, `
|
return sqlc.New(a.db).UpsertAnswer(ctx, sqlc.UpsertAnswerParams{
|
||||||
INSERT INTO answers (question_id, author_id, body, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)
|
QuestionID: a.QuestionID,
|
||||||
ON CONFLICT (question_id) DO UPDATE SET body = excluded.body, author_id = excluded.author_id, updated_at = excluded.updated_at`,
|
AuthorID: a.AuthorID,
|
||||||
a.QuestionID, a.AuthorID, a.Body, a.CreatedAt, a.UpdatedAt)
|
Body: a.Body,
|
||||||
return err
|
CreatedAt: a.CreatedAt,
|
||||||
|
UpdatedAt: a.UpdatedAt,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAnswer(ctx context.Context, db *sql.DB, questionID string) (*Answer, error) {
|
func GetAnswer(ctx context.Context, db *sql.DB, questionID string) (*Answer, error) {
|
||||||
var a Answer
|
r, err := sqlc.New(db).GetAnswer(ctx, questionID)
|
||||||
err := db.QueryRowContext(ctx, `
|
|
||||||
SELECT a.question_id, a.author_id, u.name, a.body, a.created_at, a.updated_at
|
|
||||||
FROM answers a
|
|
||||||
JOIN users u ON u.id = a.author_id
|
|
||||||
WHERE a.question_id = $1`, questionID).Scan(&a.QuestionID, &a.AuthorID, &a.AuthorName, &a.Body, &a.CreatedAt, &a.UpdatedAt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
a.db = db
|
return &Answer{
|
||||||
return &a, nil
|
QuestionID: r.QuestionID,
|
||||||
|
AuthorID: r.AuthorID,
|
||||||
|
AuthorName: r.AuthorName,
|
||||||
|
Body: r.Body,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
//go:generate make -C ../.. sqlc
|
||||||
+61
-80
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"plumber/internal/pacific"
|
"plumber/internal/pacific"
|
||||||
|
"plumber/internal/store/sqlc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RankedQuestion is a question row with score / vote annotations for lists.
|
// RankedQuestion is a question row with score / vote annotations for lists.
|
||||||
@@ -51,9 +52,15 @@ func (q *RankedQuestion) Create(ctx context.Context) error {
|
|||||||
if q.CreatedAt == "" {
|
if q.CreatedAt == "" {
|
||||||
q.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
q.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
_, err := q.db.ExecContext(ctx, `INSERT INTO questions (id, author_id, title, body, city, hunt_date, hidden, created_at) VALUES ($1, $2, $3, $4, $5, $6, 0, $7)`,
|
return sqlc.New(q.db).CreateQuestion(ctx, sqlc.CreateQuestionParams{
|
||||||
q.ID, q.AuthorID, q.Title, q.Body, q.City, q.HuntDate, q.CreatedAt)
|
ID: q.ID,
|
||||||
return err
|
AuthorID: q.AuthorID,
|
||||||
|
Title: q.Title,
|
||||||
|
Body: q.Body,
|
||||||
|
City: q.City,
|
||||||
|
HuntDate: q.HuntDate,
|
||||||
|
CreatedAt: q.CreatedAt,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide marks the question hidden.
|
// Hide marks the question hidden.
|
||||||
@@ -61,108 +68,82 @@ func (q *RankedQuestion) Hide(ctx context.Context) error {
|
|||||||
if q == nil || q.db == nil {
|
if q == nil || q.db == nil {
|
||||||
return fmt.Errorf("question: no database")
|
return fmt.Errorf("question: no database")
|
||||||
}
|
}
|
||||||
_, err := q.db.ExecContext(ctx, `UPDATE questions SET hidden = 1 WHERE id = $1`, q.ID)
|
if err := sqlc.New(q.db).HideQuestion(ctx, q.ID); err != nil {
|
||||||
if err == nil {
|
return err
|
||||||
q.Hidden = true
|
}
|
||||||
|
q.Hidden = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rankedFrom(
|
||||||
|
db *sql.DB,
|
||||||
|
id, authorID, authorName, title, body, city, huntDate, createdAt string,
|
||||||
|
hidden int32, score, answered, userVote int64,
|
||||||
|
) RankedQuestion {
|
||||||
|
return RankedQuestion{
|
||||||
|
ID: id,
|
||||||
|
AuthorID: authorID,
|
||||||
|
AuthorName: authorName,
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
City: city,
|
||||||
|
HuntDate: huntDate,
|
||||||
|
Hidden: hidden != 0,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Score: int(score),
|
||||||
|
Answered: answered != 0,
|
||||||
|
UserVote: int(userVote),
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListHunt(ctx context.Context, db *sql.DB, huntDate, viewerID string) ([]RankedQuestion, error) {
|
func ListHunt(ctx context.Context, db *sql.DB, huntDate, viewerID string) ([]RankedQuestion, error) {
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := sqlc.New(db).ListHunt(ctx, sqlc.ListHuntParams{
|
||||||
SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
UserID: viewerID,
|
||||||
COALESCE(SUM(v.value), 0) AS score,
|
HuntDate: huntDate,
|
||||||
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END AS answered,
|
})
|
||||||
COALESCE((SELECT value FROM votes WHERE user_id = $1 AND question_id = q.id), 0) AS user_vote
|
|
||||||
FROM questions q
|
|
||||||
JOIN users u ON u.id = q.author_id
|
|
||||||
LEFT JOIN votes v ON v.question_id = q.id
|
|
||||||
LEFT JOIN answers a ON a.question_id = q.id
|
|
||||||
WHERE q.hunt_date = $2 AND q.hidden = 0
|
|
||||||
GROUP BY q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, a.question_id
|
|
||||||
ORDER BY score DESC, q.created_at ASC`, viewerID, huntDate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
out := make([]RankedQuestion, 0, len(rows))
|
||||||
return scanRankedList(db, rows)
|
for _, r := range rows {
|
||||||
|
out = append(out, rankedFrom(db, r.ID, r.AuthorID, r.AuthorName, r.Title, r.Body, r.City, r.HuntDate, r.CreatedAt, r.Hidden, r.Score, r.Answered, r.UserVote))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetQuestion(ctx context.Context, db *sql.DB, id, viewerID string) (*RankedQuestion, error) {
|
func GetQuestion(ctx context.Context, db *sql.DB, id, viewerID string) (*RankedQuestion, error) {
|
||||||
row := db.QueryRowContext(ctx, `
|
r, err := sqlc.New(db).GetQuestion(ctx, sqlc.GetQuestionParams{
|
||||||
SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
UserID: viewerID,
|
||||||
COALESCE((SELECT SUM(value) FROM votes WHERE question_id = q.id), 0) AS score,
|
ID: id,
|
||||||
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END AS answered,
|
})
|
||||||
COALESCE((SELECT value FROM votes WHERE user_id = $1 AND question_id = q.id), 0) AS user_vote
|
|
||||||
FROM questions q
|
|
||||||
JOIN users u ON u.id = q.author_id
|
|
||||||
LEFT JOIN answers a ON a.question_id = q.id
|
|
||||||
WHERE q.id = $2`, viewerID, id)
|
|
||||||
q, err := scanRanked(db, row)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
q := rankedFrom(db, r.ID, r.AuthorID, r.AuthorName, r.Title, r.Body, r.City, r.HuntDate, r.CreatedAt, r.Hidden, r.Score, r.Answered, r.UserVote)
|
||||||
return &q, nil
|
return &q, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListQuestionsByAuthor(ctx context.Context, db *sql.DB, authorID string) ([]RankedQuestion, error) {
|
func ListQuestionsByAuthor(ctx context.Context, db *sql.DB, authorID string) ([]RankedQuestion, error) {
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := sqlc.New(db).ListQuestionsByAuthor(ctx, authorID)
|
||||||
SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
|
||||||
COALESCE((SELECT SUM(value) FROM votes WHERE question_id = q.id), 0) AS score,
|
|
||||||
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END AS answered,
|
|
||||||
0 AS user_vote
|
|
||||||
FROM questions q
|
|
||||||
JOIN users u ON u.id = q.author_id
|
|
||||||
LEFT JOIN answers a ON a.question_id = q.id
|
|
||||||
WHERE q.author_id = $1 AND q.hidden = 0
|
|
||||||
ORDER BY q.created_at DESC`, authorID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
out := make([]RankedQuestion, 0, len(rows))
|
||||||
return scanRankedList(db, rows)
|
for _, r := range rows {
|
||||||
|
out = append(out, rankedFrom(db, r.ID, r.AuthorID, r.AuthorName, r.Title, r.Body, r.City, r.HuntDate, r.CreatedAt, r.Hidden, r.Score, r.Answered, r.UserVote))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListQuestionsAnsweredBy(ctx context.Context, db *sql.DB, adminID string) ([]RankedQuestion, error) {
|
func ListQuestionsAnsweredBy(ctx context.Context, db *sql.DB, adminID string) ([]RankedQuestion, error) {
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := sqlc.New(db).ListQuestionsAnsweredBy(ctx, adminID)
|
||||||
SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
|
||||||
COALESCE((SELECT SUM(value) FROM votes WHERE question_id = q.id), 0) AS score,
|
|
||||||
1 AS answered,
|
|
||||||
0 AS user_vote
|
|
||||||
FROM answers ans
|
|
||||||
JOIN questions q ON q.id = ans.question_id
|
|
||||||
JOIN users u ON u.id = q.author_id
|
|
||||||
WHERE ans.author_id = $1 AND q.hidden = 0
|
|
||||||
ORDER BY ans.updated_at DESC`, adminID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
out := make([]RankedQuestion, 0, len(rows))
|
||||||
return scanRankedList(db, rows)
|
for _, r := range rows {
|
||||||
}
|
out = append(out, rankedFrom(db, r.ID, r.AuthorID, r.AuthorName, r.Title, r.Body, r.City, r.HuntDate, r.CreatedAt, r.Hidden, r.Score, r.Answered, r.UserVote))
|
||||||
|
|
||||||
type scanned interface {
|
|
||||||
Scan(dest ...any) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanRanked(db *sql.DB, rows scanned) (RankedQuestion, error) {
|
|
||||||
var q RankedQuestion
|
|
||||||
var hidden, answered int
|
|
||||||
err := rows.Scan(&q.ID, &q.AuthorID, &q.AuthorName, &q.Title, &q.Body, &q.City, &q.HuntDate, &hidden, &q.CreatedAt, &q.Score, &answered, &q.UserVote)
|
|
||||||
q.Hidden = hidden != 0
|
|
||||||
q.Answered = answered != 0
|
|
||||||
q.db = db
|
|
||||||
return q, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanRankedList(db *sql.DB, rows *sql.Rows) ([]RankedQuestion, error) {
|
|
||||||
var out []RankedQuestion
|
|
||||||
for rows.Next() {
|
|
||||||
q, err := scanRanked(db, rows)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out = append(out, q)
|
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: answers.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getAnswer = `-- name: GetAnswer :one
|
||||||
|
SELECT a.question_id, a.author_id, u.name AS author_name, a.body, a.created_at, a.updated_at
|
||||||
|
FROM answers a
|
||||||
|
JOIN users u ON u.id = a.author_id
|
||||||
|
WHERE a.question_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAnswerRow struct {
|
||||||
|
QuestionID string
|
||||||
|
AuthorID string
|
||||||
|
AuthorName string
|
||||||
|
Body string
|
||||||
|
CreatedAt string
|
||||||
|
UpdatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAnswer(ctx context.Context, questionID string) (GetAnswerRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getAnswer, questionID)
|
||||||
|
var i GetAnswerRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.AuthorID,
|
||||||
|
&i.AuthorName,
|
||||||
|
&i.Body,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertAnswer = `-- name: UpsertAnswer :exec
|
||||||
|
INSERT INTO answers (question_id, author_id, body, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
ON CONFLICT (question_id) DO UPDATE
|
||||||
|
SET body = excluded.body, author_id = excluded.author_id, updated_at = excluded.updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertAnswerParams struct {
|
||||||
|
QuestionID string
|
||||||
|
AuthorID string
|
||||||
|
Body string
|
||||||
|
CreatedAt string
|
||||||
|
UpdatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertAnswer(ctx context.Context, arg UpsertAnswerParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertAnswer,
|
||||||
|
arg.QuestionID,
|
||||||
|
arg.AuthorID,
|
||||||
|
arg.Body,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
type Answer struct {
|
||||||
|
QuestionID string
|
||||||
|
AuthorID string
|
||||||
|
Body string
|
||||||
|
CreatedAt string
|
||||||
|
UpdatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Question struct {
|
||||||
|
ID string
|
||||||
|
AuthorID string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
City string
|
||||||
|
HuntDate string
|
||||||
|
Hidden int32
|
||||||
|
CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
PasswordHash string
|
||||||
|
Role string
|
||||||
|
AvatarUrl string
|
||||||
|
State string
|
||||||
|
CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vote struct {
|
||||||
|
UserID string
|
||||||
|
QuestionID string
|
||||||
|
Value int32
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: questions.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createQuestion = `-- name: CreateQuestion :exec
|
||||||
|
INSERT INTO questions (id, author_id, title, body, city, hunt_date, hidden, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, 0, $7)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateQuestionParams struct {
|
||||||
|
ID string
|
||||||
|
AuthorID string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
City string
|
||||||
|
HuntDate string
|
||||||
|
CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createQuestion,
|
||||||
|
arg.ID,
|
||||||
|
arg.AuthorID,
|
||||||
|
arg.Title,
|
||||||
|
arg.Body,
|
||||||
|
arg.City,
|
||||||
|
arg.HuntDate,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQuestion = `-- name: GetQuestion :one
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE((SELECT SUM(value) FROM votes WHERE votes.question_id = q.id), 0)::bigint AS score,
|
||||||
|
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END::bigint AS answered,
|
||||||
|
COALESCE((SELECT value FROM votes WHERE votes.user_id = $1 AND votes.question_id = q.id), 0)::bigint AS user_vote
|
||||||
|
FROM questions q
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
LEFT JOIN answers a ON a.question_id = q.id
|
||||||
|
WHERE q.id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetQuestionParams struct {
|
||||||
|
UserID string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetQuestionRow struct {
|
||||||
|
ID string
|
||||||
|
AuthorID string
|
||||||
|
AuthorName string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
City string
|
||||||
|
HuntDate string
|
||||||
|
Hidden int32
|
||||||
|
CreatedAt string
|
||||||
|
Score int64
|
||||||
|
Answered int64
|
||||||
|
UserVote int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetQuestion(ctx context.Context, arg GetQuestionParams) (GetQuestionRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getQuestion, arg.UserID, arg.ID)
|
||||||
|
var i GetQuestionRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AuthorID,
|
||||||
|
&i.AuthorName,
|
||||||
|
&i.Title,
|
||||||
|
&i.Body,
|
||||||
|
&i.City,
|
||||||
|
&i.HuntDate,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Score,
|
||||||
|
&i.Answered,
|
||||||
|
&i.UserVote,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideQuestion = `-- name: HideQuestion :exec
|
||||||
|
UPDATE questions
|
||||||
|
SET hidden = 1
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) HideQuestion(ctx context.Context, id string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, hideQuestion, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listHunt = `-- name: ListHunt :many
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE(SUM(v.value), 0)::bigint AS score,
|
||||||
|
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END::bigint AS answered,
|
||||||
|
COALESCE((SELECT value FROM votes WHERE votes.user_id = $1 AND votes.question_id = q.id), 0)::bigint AS user_vote
|
||||||
|
FROM questions q
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
LEFT JOIN votes v ON v.question_id = q.id
|
||||||
|
LEFT JOIN answers a ON a.question_id = q.id
|
||||||
|
WHERE q.hunt_date = $2 AND q.hidden = 0
|
||||||
|
GROUP BY q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, a.question_id
|
||||||
|
ORDER BY score DESC, q.created_at ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListHuntParams struct {
|
||||||
|
UserID string
|
||||||
|
HuntDate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListHuntRow struct {
|
||||||
|
ID string
|
||||||
|
AuthorID string
|
||||||
|
AuthorName string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
City string
|
||||||
|
HuntDate string
|
||||||
|
Hidden int32
|
||||||
|
CreatedAt string
|
||||||
|
Score int64
|
||||||
|
Answered int64
|
||||||
|
UserVote int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListHunt(ctx context.Context, arg ListHuntParams) ([]ListHuntRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listHunt, arg.UserID, arg.HuntDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListHuntRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListHuntRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AuthorID,
|
||||||
|
&i.AuthorName,
|
||||||
|
&i.Title,
|
||||||
|
&i.Body,
|
||||||
|
&i.City,
|
||||||
|
&i.HuntDate,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Score,
|
||||||
|
&i.Answered,
|
||||||
|
&i.UserVote,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listQuestionsAnsweredBy = `-- name: ListQuestionsAnsweredBy :many
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE((SELECT SUM(value) FROM votes WHERE votes.question_id = q.id), 0)::bigint AS score,
|
||||||
|
1::bigint AS answered,
|
||||||
|
0::bigint AS user_vote
|
||||||
|
FROM answers ans
|
||||||
|
JOIN questions q ON q.id = ans.question_id
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
WHERE ans.author_id = $1 AND q.hidden = 0
|
||||||
|
ORDER BY ans.updated_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListQuestionsAnsweredByRow struct {
|
||||||
|
ID string
|
||||||
|
AuthorID string
|
||||||
|
AuthorName string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
City string
|
||||||
|
HuntDate string
|
||||||
|
Hidden int32
|
||||||
|
CreatedAt string
|
||||||
|
Score int64
|
||||||
|
Answered int64
|
||||||
|
UserVote int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListQuestionsAnsweredBy(ctx context.Context, authorID string) ([]ListQuestionsAnsweredByRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listQuestionsAnsweredBy, authorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListQuestionsAnsweredByRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListQuestionsAnsweredByRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AuthorID,
|
||||||
|
&i.AuthorName,
|
||||||
|
&i.Title,
|
||||||
|
&i.Body,
|
||||||
|
&i.City,
|
||||||
|
&i.HuntDate,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Score,
|
||||||
|
&i.Answered,
|
||||||
|
&i.UserVote,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listQuestionsByAuthor = `-- name: ListQuestionsByAuthor :many
|
||||||
|
SELECT q.id, q.author_id, u.name AS author_name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at,
|
||||||
|
COALESCE((SELECT SUM(value) FROM votes WHERE votes.question_id = q.id), 0)::bigint AS score,
|
||||||
|
CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END::bigint AS answered,
|
||||||
|
0::bigint AS user_vote
|
||||||
|
FROM questions q
|
||||||
|
JOIN users u ON u.id = q.author_id
|
||||||
|
LEFT JOIN answers a ON a.question_id = q.id
|
||||||
|
WHERE q.author_id = $1 AND q.hidden = 0
|
||||||
|
ORDER BY q.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListQuestionsByAuthorRow struct {
|
||||||
|
ID string
|
||||||
|
AuthorID string
|
||||||
|
AuthorName string
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
City string
|
||||||
|
HuntDate string
|
||||||
|
Hidden int32
|
||||||
|
CreatedAt string
|
||||||
|
Score int64
|
||||||
|
Answered int64
|
||||||
|
UserVote int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListQuestionsByAuthor(ctx context.Context, authorID string) ([]ListQuestionsByAuthorRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listQuestionsByAuthor, authorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListQuestionsByAuthorRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListQuestionsByAuthorRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AuthorID,
|
||||||
|
&i.AuthorName,
|
||||||
|
&i.Title,
|
||||||
|
&i.Body,
|
||||||
|
&i.City,
|
||||||
|
&i.HuntDate,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.Score,
|
||||||
|
&i.Answered,
|
||||||
|
&i.UserVote,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: users.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const countAdmins = `-- name: CountAdmins :one
|
||||||
|
SELECT COUNT(*)::bigint AS count
|
||||||
|
FROM users
|
||||||
|
WHERE role = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountAdmins(ctx context.Context, role string) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, countAdmins, role)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUser = `-- name: CreateUser :exec
|
||||||
|
INSERT INTO users (id, username, name, password_hash, role, avatar_url, state, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, '', '', $6)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateUserParams struct {
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
PasswordHash string
|
||||||
|
Role string
|
||||||
|
CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createUser,
|
||||||
|
arg.ID,
|
||||||
|
arg.Username,
|
||||||
|
arg.Name,
|
||||||
|
arg.PasswordHash,
|
||||||
|
arg.Role,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
|
SELECT id, username, name, role, avatar_url, state, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetUserByIDRow struct {
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
Role string
|
||||||
|
AvatarUrl string
|
||||||
|
State string
|
||||||
|
CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByID(ctx context.Context, id string) (GetUserByIDRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||||
|
var i GetUserByIDRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Name,
|
||||||
|
&i.Role,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.State,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||||
|
SELECT id, username, name, role, avatar_url, state, created_at, password_hash
|
||||||
|
FROM users
|
||||||
|
WHERE username = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetUserByUsernameRow struct {
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
Role string
|
||||||
|
AvatarUrl string
|
||||||
|
State string
|
||||||
|
CreatedAt string
|
||||||
|
PasswordHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserByUsername, username)
|
||||||
|
var i GetUserByUsernameRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Name,
|
||||||
|
&i.Role,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.State,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.PasswordHash,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserRole = `-- name: GetUserRole :one
|
||||||
|
SELECT role
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserRole(ctx context.Context, id string) (string, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserRole, id)
|
||||||
|
var role string
|
||||||
|
err := row.Scan(&role)
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listUsers = `-- name: ListUsers :many
|
||||||
|
SELECT id, username, name, role, avatar_url, state, created_at
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListUsersRow struct {
|
||||||
|
ID string
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
Role string
|
||||||
|
AvatarUrl string
|
||||||
|
State string
|
||||||
|
CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listUsers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListUsersRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListUsersRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Name,
|
||||||
|
&i.Role,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.State,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserRole = `-- name: UpdateUserRole :execresult
|
||||||
|
UPDATE users
|
||||||
|
SET role = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserRoleParams struct {
|
||||||
|
Role string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (sql.Result, error) {
|
||||||
|
return q.db.ExecContext(ctx, updateUserRole, arg.Role, arg.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserState = `-- name: UpdateUserState :exec
|
||||||
|
UPDATE users
|
||||||
|
SET state = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserStateParams struct {
|
||||||
|
State string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserState(ctx context.Context, arg UpdateUserStateParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateUserState, arg.State, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserStateAndAvatar = `-- name: UpdateUserStateAndAvatar :exec
|
||||||
|
UPDATE users
|
||||||
|
SET state = $1, avatar_url = $2
|
||||||
|
WHERE id = $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserStateAndAvatarParams struct {
|
||||||
|
State string
|
||||||
|
AvatarUrl string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserStateAndAvatar(ctx context.Context, arg UpdateUserStateAndAvatarParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateUserStateAndAvatar, arg.State, arg.AvatarUrl, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: votes.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteVote = `-- name: DeleteVote :exec
|
||||||
|
DELETE FROM votes
|
||||||
|
WHERE user_id = $1 AND question_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteVoteParams struct {
|
||||||
|
UserID string
|
||||||
|
QuestionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteVote(ctx context.Context, arg DeleteVoteParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteVote, arg.UserID, arg.QuestionID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVote = `-- name: GetVote :one
|
||||||
|
SELECT value
|
||||||
|
FROM votes
|
||||||
|
WHERE user_id = $1 AND question_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetVoteParams struct {
|
||||||
|
UserID string
|
||||||
|
QuestionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetVote(ctx context.Context, arg GetVoteParams) (int32, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getVote, arg.UserID, arg.QuestionID)
|
||||||
|
var value int32
|
||||||
|
err := row.Scan(&value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertVote = `-- name: UpsertVote :exec
|
||||||
|
INSERT INTO votes (user_id, question_id, value)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (user_id, question_id) DO UPDATE
|
||||||
|
SET value = excluded.value
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertVoteParams struct {
|
||||||
|
UserID string
|
||||||
|
QuestionID string
|
||||||
|
Value int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertVote(ctx context.Context, arg UpsertVoteParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertVote, arg.UserID, arg.QuestionID, arg.Value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
+58
-48
@@ -9,6 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"plumber/internal/store/sqlc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrLastAdmin is returned when demoting the only remaining admin.
|
// ErrLastAdmin is returned when demoting the only remaining admin.
|
||||||
@@ -22,7 +24,7 @@ const (
|
|||||||
RoleAdmin Role = "admin"
|
RoleAdmin Role = "admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User is an account row. Methods run SQL against db.
|
// User is an account row. Methods run SQL against db via sqlc.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string
|
ID string
|
||||||
Username string
|
Username string
|
||||||
@@ -48,6 +50,20 @@ func NormalizeUsername(s string) string {
|
|||||||
return strings.ToLower(strings.TrimSpace(s))
|
return strings.ToLower(strings.TrimSpace(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toUser(db *sql.DB, id, username, name, role, avatarURL, state, createdAt, passwordHash string) *User {
|
||||||
|
return &User{
|
||||||
|
ID: id,
|
||||||
|
Username: username,
|
||||||
|
Name: name,
|
||||||
|
Role: Role(role),
|
||||||
|
AvatarURL: avatarURL,
|
||||||
|
State: state,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create inserts the user. Sets ID, Name, and CreatedAt when empty.
|
// Create inserts the user. Sets ID, Name, and CreatedAt when empty.
|
||||||
func (u *User) Create(ctx context.Context) error {
|
func (u *User) Create(ctx context.Context) error {
|
||||||
if u == nil || u.db == nil {
|
if u == nil || u.db == nil {
|
||||||
@@ -66,9 +82,14 @@ func (u *User) Create(ctx context.Context) error {
|
|||||||
if u.CreatedAt == "" {
|
if u.CreatedAt == "" {
|
||||||
u.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
u.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
_, err := u.db.ExecContext(ctx, `INSERT INTO users (id, username, name, password_hash, role, avatar_url, state, created_at) VALUES ($1, $2, $3, $4, $5, '', '', $6)`,
|
return sqlc.New(u.db).CreateUser(ctx, sqlc.CreateUserParams{
|
||||||
u.ID, u.Username, u.Name, u.PasswordHash, string(u.Role), u.CreatedAt)
|
ID: u.ID,
|
||||||
return err
|
Username: u.Username,
|
||||||
|
Name: u.Name,
|
||||||
|
PasswordHash: u.PasswordHash,
|
||||||
|
Role: string(u.Role),
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRole updates this user's role (last-admin safe).
|
// SetRole updates this user's role (last-admin safe).
|
||||||
@@ -85,21 +106,24 @@ func (u *User) SetRole(ctx context.Context, role Role) error {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
var current string
|
q := sqlc.New(tx)
|
||||||
err = tx.QueryRowContext(ctx, `SELECT role FROM users WHERE id = $1`, u.ID).Scan(¤t)
|
current, err := q.GetUserRole(ctx, u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if Role(current) == RoleAdmin && role == RoleUser {
|
if Role(current) == RoleAdmin && role == RoleUser {
|
||||||
var n int
|
n, err := q.CountAdmins(ctx, string(RoleAdmin))
|
||||||
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = $1`, string(RoleAdmin)).Scan(&n); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if n <= 1 {
|
if n <= 1 {
|
||||||
return ErrLastAdmin
|
return ErrLastAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res, err := tx.ExecContext(ctx, `UPDATE users SET role = $1 WHERE id = $2`, string(role), u.ID)
|
res, err := q.UpdateUserRole(ctx, sqlc.UpdateUserRoleParams{
|
||||||
|
Role: string(role),
|
||||||
|
ID: u.ID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -123,61 +147,47 @@ func (u *User) SaveProfile(ctx context.Context) error {
|
|||||||
return fmt.Errorf("user: no database")
|
return fmt.Errorf("user: no database")
|
||||||
}
|
}
|
||||||
u.State = strings.TrimSpace(u.State)
|
u.State = strings.TrimSpace(u.State)
|
||||||
|
q := sqlc.New(u.db)
|
||||||
if u.AvatarURL == "" {
|
if u.AvatarURL == "" {
|
||||||
_, err := u.db.ExecContext(ctx, `UPDATE users SET state = $1 WHERE id = $2`, u.State, u.ID)
|
return q.UpdateUserState(ctx, sqlc.UpdateUserStateParams{State: u.State, ID: u.ID})
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
_, err := u.db.ExecContext(ctx, `UPDATE users SET state = $1, avatar_url = $2 WHERE id = $3`, u.State, u.AvatarURL, u.ID)
|
return q.UpdateUserStateAndAvatar(ctx, sqlc.UpdateUserStateAndAvatarParams{
|
||||||
return err
|
State: u.State,
|
||||||
|
AvatarUrl: u.AvatarURL,
|
||||||
|
ID: u.ID,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountAdmins(ctx context.Context, db *sql.DB) (int, error) {
|
func CountAdmins(ctx context.Context, db *sql.DB) (int, error) {
|
||||||
var n int
|
n, err := sqlc.New(db).CountAdmins(ctx, string(RoleAdmin))
|
||||||
err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = $1`, string(RoleAdmin)).Scan(&n)
|
return int(n), err
|
||||||
return n, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListUsers(ctx context.Context, db *sql.DB) ([]User, error) {
|
func ListUsers(ctx context.Context, db *sql.DB) ([]User, error) {
|
||||||
rows, err := db.QueryContext(ctx, `SELECT id, username, name, role, avatar_url, state, created_at FROM users ORDER BY created_at ASC`)
|
rows, err := sqlc.New(db).ListUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
out := make([]User, 0, len(rows))
|
||||||
var out []User
|
for _, r := range rows {
|
||||||
for rows.Next() {
|
u := toUser(db, r.ID, r.Username, r.Name, r.Role, r.AvatarUrl, r.State, r.CreatedAt, "")
|
||||||
var u User
|
out = append(out, *u)
|
||||||
var role string
|
|
||||||
if err := rows.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u.Role = Role(role)
|
|
||||||
u.db = db
|
|
||||||
out = append(out, u)
|
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserByID(ctx context.Context, db *sql.DB, id string) (*User, error) {
|
func UserByID(ctx context.Context, db *sql.DB, id string) (*User, error) {
|
||||||
return scanUser(db, db.QueryRowContext(ctx, `SELECT id, username, name, role, avatar_url, state, created_at FROM users WHERE id = $1`, id), false)
|
r, err := sqlc.New(db).GetUserByID(ctx, id)
|
||||||
}
|
|
||||||
|
|
||||||
func UserByUsername(ctx context.Context, db *sql.DB, username string) (*User, error) {
|
|
||||||
return scanUser(db, db.QueryRowContext(ctx, `SELECT id, username, name, role, avatar_url, state, created_at, password_hash FROM users WHERE username = $1`, NormalizeUsername(username)), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanUser(db *sql.DB, row *sql.Row, withSecrets bool) (*User, error) {
|
|
||||||
var u User
|
|
||||||
var role string
|
|
||||||
var err error
|
|
||||||
if withSecrets {
|
|
||||||
err = row.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt, &u.PasswordHash)
|
|
||||||
} else {
|
|
||||||
err = row.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u.Role = Role(role)
|
return toUser(db, r.ID, r.Username, r.Name, r.Role, r.AvatarUrl, r.State, r.CreatedAt, ""), nil
|
||||||
u.db = db
|
}
|
||||||
return &u, nil
|
|
||||||
|
func UserByUsername(ctx context.Context, db *sql.DB, username string) (*User, error) {
|
||||||
|
r, err := sqlc.New(db).GetUserByUsername(ctx, NormalizeUsername(username))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toUser(db, r.ID, r.Username, r.Name, r.Role, r.AvatarUrl, r.State, r.CreatedAt, r.PasswordHash), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-6
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"plumber/internal/store/sqlc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vote toggles or sets a user's vote on a question (value must be 1 or -1).
|
// Vote toggles or sets a user's vote on a question (value must be 1 or -1).
|
||||||
@@ -16,16 +18,20 @@ func Vote(ctx context.Context, db *sql.DB, userID, questionID string, value int)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
var current sql.NullInt64
|
|
||||||
err = tx.QueryRowContext(ctx, `SELECT value FROM votes WHERE user_id = $1 AND question_id = $2`, userID, questionID).Scan(¤t)
|
q := sqlc.New(tx)
|
||||||
|
current, err := q.GetVote(ctx, sqlc.GetVoteParams{UserID: userID, QuestionID: questionID})
|
||||||
if err != nil && err != sql.ErrNoRows {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err == nil && current.Valid && int(current.Int64) == value {
|
if err == nil && int(current) == value {
|
||||||
_, err = tx.ExecContext(ctx, `DELETE FROM votes WHERE user_id = $1 AND question_id = $2`, userID, questionID)
|
err = q.DeleteVote(ctx, sqlc.DeleteVoteParams{UserID: userID, QuestionID: questionID})
|
||||||
} else {
|
} else {
|
||||||
_, err = tx.ExecContext(ctx, `INSERT INTO votes (user_id, question_id, value) VALUES ($1, $2, $3)
|
err = q.UpsertVote(ctx, sqlc.UpsertVoteParams{
|
||||||
ON CONFLICT (user_id, question_id) DO UPDATE SET value = excluded.value`, userID, questionID, value)
|
UserID: userID,
|
||||||
|
QuestionID: questionID,
|
||||||
|
Value: int32(value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user