diff options
| -rw-r--r-- | backend/db/articles.sql.go | 350 | ||||
| -rw-r--r-- | backend/db/db.go | 31 | ||||
| -rw-r--r-- | backend/db/feeds.sql.go | 165 | ||||
| -rw-r--r-- | backend/db/models.go | 21 | ||||
| -rw-r--r-- | backend/db/queries/articles.sql | 73 | ||||
| -rw-r--r-- | backend/db/queries/feeds.sql | 32 | ||||
| -rw-r--r-- | backend/db/schema.sql | 25 | ||||
| -rw-r--r-- | backend/go.mod | 49 | ||||
| -rw-r--r-- | backend/go.sum | 166 | ||||
| -rw-r--r-- | backend/gqlgen.yml | 6 | ||||
| -rw-r--r-- | backend/graphql/resolver/resolver.go | 15 | ||||
| -rw-r--r-- | backend/graphql/resolver/schema.resolvers.go (renamed from backend/graphql/resolvers.go) | 236 | ||||
| -rw-r--r-- | backend/justfile | 5 | ||||
| -rw-r--r-- | backend/main.go | 95 | ||||
| -rw-r--r-- | backend/sqlc.yaml | 13 |
15 files changed, 1088 insertions, 194 deletions
diff --git a/backend/db/articles.sql.go b/backend/db/articles.sql.go new file mode 100644 index 0000000..9e60cb4 --- /dev/null +++ b/backend/db/articles.sql.go @@ -0,0 +1,350 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: articles.sql + +package db + +import ( + "context" +) + +const checkArticleExists = `-- name: CheckArticleExists :one +SELECT EXISTS( + SELECT 1 FROM articles + WHERE feed_id = ? AND guid = ? +) as article_exists +` + +type CheckArticleExistsParams struct { + FeedID int64 + Guid string +} + +func (q *Queries) CheckArticleExists(ctx context.Context, arg CheckArticleExistsParams) (int64, error) { + row := q.db.QueryRowContext(ctx, checkArticleExists, arg.FeedID, arg.Guid) + var article_exists int64 + err := row.Scan(&article_exists) + return article_exists, err +} + +const createArticle = `-- name: CreateArticle :one +INSERT INTO articles (feed_id, guid, title, url, is_read) +VALUES (?, ?, ?, ?, ?) +RETURNING id, feed_id, guid, title, url, is_read +` + +type CreateArticleParams struct { + FeedID int64 + Guid string + Title string + Url string + IsRead int64 +} + +func (q *Queries) CreateArticle(ctx context.Context, arg CreateArticleParams) (Article, error) { + row := q.db.QueryRowContext(ctx, createArticle, + arg.FeedID, + arg.Guid, + arg.Title, + arg.Url, + arg.IsRead, + ) + var i Article + err := row.Scan( + &i.ID, + &i.FeedID, + &i.Guid, + &i.Title, + &i.Url, + &i.IsRead, + ) + return i, err +} + +const deleteArticlesByFeed = `-- name: DeleteArticlesByFeed :exec +DELETE FROM articles +WHERE feed_id = ? +` + +func (q *Queries) DeleteArticlesByFeed(ctx context.Context, feedID int64) error { + _, err := q.db.ExecContext(ctx, deleteArticlesByFeed, feedID) + return err +} + +const getArticle = `-- name: GetArticle :one +SELECT + a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, + f.id as feed_id_2, f.url as feed_url, f.title as feed_title +FROM articles AS a +INNER JOIN feeds AS f ON a.feed_id = f.id +WHERE a.id = ? +` + +type GetArticleRow struct { + ID int64 + FeedID int64 + Guid string + Title string + Url string + IsRead int64 + FeedID2 int64 + FeedUrl string + FeedTitle string +} + +func (q *Queries) GetArticle(ctx context.Context, id int64) (GetArticleRow, error) { + row := q.db.QueryRowContext(ctx, getArticle, id) + var i GetArticleRow + err := row.Scan( + &i.ID, + &i.FeedID, + &i.Guid, + &i.Title, + &i.Url, + &i.IsRead, + &i.FeedID2, + &i.FeedUrl, + &i.FeedTitle, + ) + return i, err +} + +const getArticleGUIDsByFeed = `-- name: GetArticleGUIDsByFeed :many +SELECT guid +FROM articles +WHERE feed_id = ? +` + +func (q *Queries) GetArticleGUIDsByFeed(ctx context.Context, feedID int64) ([]string, error) { + rows, err := q.db.QueryContext(ctx, getArticleGUIDsByFeed, feedID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []string{} + for rows.Next() { + var guid string + if err := rows.Scan(&guid); err != nil { + return nil, err + } + items = append(items, guid) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getArticlesByFeed = `-- name: GetArticlesByFeed :many +SELECT id, feed_id, guid, title, url, is_read +FROM articles +WHERE feed_id = ? +ORDER BY id DESC +` + +func (q *Queries) GetArticlesByFeed(ctx context.Context, feedID int64) ([]Article, error) { + rows, err := q.db.QueryContext(ctx, getArticlesByFeed, feedID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Article{} + for rows.Next() { + var i Article + if err := rows.Scan( + &i.ID, + &i.FeedID, + &i.Guid, + &i.Title, + &i.Url, + &i.IsRead, + ); 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 getReadArticles = `-- name: GetReadArticles :many +SELECT + a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, + f.id as feed_id_2, f.url as feed_url, f.title as feed_title +FROM articles AS a +INNER JOIN feeds AS f ON a.feed_id = f.id +WHERE a.is_read = 1 +ORDER BY a.id DESC +LIMIT 100 +` + +type GetReadArticlesRow struct { + ID int64 + FeedID int64 + Guid string + Title string + Url string + IsRead int64 + FeedID2 int64 + FeedUrl string + FeedTitle string +} + +func (q *Queries) GetReadArticles(ctx context.Context) ([]GetReadArticlesRow, error) { + rows, err := q.db.QueryContext(ctx, getReadArticles) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetReadArticlesRow{} + for rows.Next() { + var i GetReadArticlesRow + if err := rows.Scan( + &i.ID, + &i.FeedID, + &i.Guid, + &i.Title, + &i.Url, + &i.IsRead, + &i.FeedID2, + &i.FeedUrl, + &i.FeedTitle, + ); 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 getUnreadArticles = `-- name: GetUnreadArticles :many +SELECT + a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, + f.id as feed_id_2, f.url as feed_url, f.title as feed_title +FROM articles AS a +INNER JOIN feeds AS f ON a.feed_id = f.id +WHERE a.is_read = 0 +ORDER BY a.id DESC +LIMIT 100 +` + +type GetUnreadArticlesRow struct { + ID int64 + FeedID int64 + Guid string + Title string + Url string + IsRead int64 + FeedID2 int64 + FeedUrl string + FeedTitle string +} + +func (q *Queries) GetUnreadArticles(ctx context.Context) ([]GetUnreadArticlesRow, error) { + rows, err := q.db.QueryContext(ctx, getUnreadArticles) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetUnreadArticlesRow{} + for rows.Next() { + var i GetUnreadArticlesRow + if err := rows.Scan( + &i.ID, + &i.FeedID, + &i.Guid, + &i.Title, + &i.Url, + &i.IsRead, + &i.FeedID2, + &i.FeedUrl, + &i.FeedTitle, + ); 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 markFeedArticlesRead = `-- name: MarkFeedArticlesRead :exec +UPDATE articles +SET is_read = 1 +WHERE feed_id = ? +` + +func (q *Queries) MarkFeedArticlesRead(ctx context.Context, feedID int64) error { + _, err := q.db.ExecContext(ctx, markFeedArticlesRead, feedID) + return err +} + +const markFeedArticlesUnread = `-- name: MarkFeedArticlesUnread :exec +UPDATE articles +SET is_read = 0 +WHERE feed_id = ? +` + +func (q *Queries) MarkFeedArticlesUnread(ctx context.Context, feedID int64) error { + _, err := q.db.ExecContext(ctx, markFeedArticlesUnread, feedID) + return err +} + +const updateArticle = `-- name: UpdateArticle :exec +UPDATE articles +SET title = ?, url = ? +WHERE feed_id = ? AND guid = ? +` + +type UpdateArticleParams struct { + Title string + Url string + FeedID int64 + Guid string +} + +func (q *Queries) UpdateArticle(ctx context.Context, arg UpdateArticleParams) error { + _, err := q.db.ExecContext(ctx, updateArticle, + arg.Title, + arg.Url, + arg.FeedID, + arg.Guid, + ) + return err +} + +const updateArticleReadStatus = `-- name: UpdateArticleReadStatus :exec +UPDATE articles +SET is_read = ? +WHERE id = ? +` + +type UpdateArticleReadStatusParams struct { + IsRead int64 + ID int64 +} + +func (q *Queries) UpdateArticleReadStatus(ctx context.Context, arg UpdateArticleReadStatusParams) error { + _, err := q.db.ExecContext(ctx, updateArticleReadStatus, arg.IsRead, arg.ID) + return err +} diff --git a/backend/db/db.go b/backend/db/db.go new file mode 100644 index 0000000..0c56c2b --- /dev/null +++ b/backend/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +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, + } +} diff --git a/backend/db/feeds.sql.go b/backend/db/feeds.sql.go new file mode 100644 index 0000000..4db84af --- /dev/null +++ b/backend/db/feeds.sql.go @@ -0,0 +1,165 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: feeds.sql + +package db + +import ( + "context" +) + +const createFeed = `-- name: CreateFeed :one +INSERT INTO feeds (url, title, fetched_at) +VALUES (?, ?, ?) +RETURNING id, url, title, fetched_at +` + +type CreateFeedParams struct { + Url string + Title string + FetchedAt string +} + +func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) { + row := q.db.QueryRowContext(ctx, createFeed, arg.Url, arg.Title, arg.FetchedAt) + var i Feed + err := row.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.FetchedAt, + ) + return i, err +} + +const deleteFeed = `-- name: DeleteFeed :exec +DELETE FROM feeds +WHERE id = ? +` + +func (q *Queries) DeleteFeed(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteFeed, id) + return err +} + +const getFeed = `-- name: GetFeed :one +SELECT id, url, title, fetched_at +FROM feeds +WHERE id = ? +` + +func (q *Queries) GetFeed(ctx context.Context, id int64) (Feed, error) { + row := q.db.QueryRowContext(ctx, getFeed, id) + var i Feed + err := row.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.FetchedAt, + ) + return i, err +} + +const getFeedByURL = `-- name: GetFeedByURL :one +SELECT id, url, title, fetched_at +FROM feeds +WHERE url = ? +` + +func (q *Queries) GetFeedByURL(ctx context.Context, url string) (Feed, error) { + row := q.db.QueryRowContext(ctx, getFeedByURL, url) + var i Feed + err := row.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.FetchedAt, + ) + return i, err +} + +const getFeeds = `-- name: GetFeeds :many +SELECT id, url, title, fetched_at +FROM feeds +ORDER BY id +` + +func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getFeeds) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Feed{} + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.FetchedAt, + ); 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 getFeedsToFetch = `-- name: GetFeedsToFetch :many +SELECT id, url, fetched_at +FROM feeds +` + +type GetFeedsToFetchRow struct { + ID int64 + Url string + FetchedAt string +} + +func (q *Queries) GetFeedsToFetch(ctx context.Context) ([]GetFeedsToFetchRow, error) { + rows, err := q.db.QueryContext(ctx, getFeedsToFetch) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetFeedsToFetchRow{} + for rows.Next() { + var i GetFeedsToFetchRow + if err := rows.Scan(&i.ID, &i.Url, &i.FetchedAt); 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 updateFeedMetadata = `-- name: UpdateFeedMetadata :exec +UPDATE feeds +SET title = ?, fetched_at = ? +WHERE id = ? +` + +type UpdateFeedMetadataParams struct { + Title string + FetchedAt string + ID int64 +} + +func (q *Queries) UpdateFeedMetadata(ctx context.Context, arg UpdateFeedMetadataParams) error { + _, err := q.db.ExecContext(ctx, updateFeedMetadata, arg.Title, arg.FetchedAt, arg.ID) + return err +} diff --git a/backend/db/models.go b/backend/db/models.go new file mode 100644 index 0000000..2f36cb4 --- /dev/null +++ b/backend/db/models.go @@ -0,0 +1,21 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +type Article struct { + ID int64 + FeedID int64 + Guid string + Title string + Url string + IsRead int64 +} + +type Feed struct { + ID int64 + Url string + Title string + FetchedAt string +} diff --git a/backend/db/queries/articles.sql b/backend/db/queries/articles.sql new file mode 100644 index 0000000..3f1590a --- /dev/null +++ b/backend/db/queries/articles.sql @@ -0,0 +1,73 @@ +-- name: GetArticle :one +SELECT + a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, + f.id as feed_id_2, f.url as feed_url, f.title as feed_title +FROM articles AS a +INNER JOIN feeds AS f ON a.feed_id = f.id +WHERE a.id = ?; + +-- name: GetUnreadArticles :many +SELECT + a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, + f.id as feed_id_2, f.url as feed_url, f.title as feed_title +FROM articles AS a +INNER JOIN feeds AS f ON a.feed_id = f.id +WHERE a.is_read = 0 +ORDER BY a.id DESC +LIMIT 100; + +-- name: GetReadArticles :many +SELECT + a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, + f.id as feed_id_2, f.url as feed_url, f.title as feed_title +FROM articles AS a +INNER JOIN feeds AS f ON a.feed_id = f.id +WHERE a.is_read = 1 +ORDER BY a.id DESC +LIMIT 100; + +-- name: GetArticlesByFeed :many +SELECT id, feed_id, guid, title, url, is_read +FROM articles +WHERE feed_id = ? +ORDER BY id DESC; + +-- name: GetArticleGUIDsByFeed :many +SELECT guid +FROM articles +WHERE feed_id = ?; + +-- name: CreateArticle :one +INSERT INTO articles (feed_id, guid, title, url, is_read) +VALUES (?, ?, ?, ?, ?) +RETURNING *; + +-- name: UpdateArticle :exec +UPDATE articles +SET title = ?, url = ? +WHERE feed_id = ? AND guid = ?; + +-- name: UpdateArticleReadStatus :exec +UPDATE articles +SET is_read = ? +WHERE id = ?; + +-- name: MarkFeedArticlesRead :exec +UPDATE articles +SET is_read = 1 +WHERE feed_id = ?; + +-- name: MarkFeedArticlesUnread :exec +UPDATE articles +SET is_read = 0 +WHERE feed_id = ?; + +-- name: DeleteArticlesByFeed :exec +DELETE FROM articles +WHERE feed_id = ?; + +-- name: CheckArticleExists :one +SELECT EXISTS( + SELECT 1 FROM articles + WHERE feed_id = ? AND guid = ? +) as article_exists; diff --git a/backend/db/queries/feeds.sql b/backend/db/queries/feeds.sql new file mode 100644 index 0000000..6d4d172 --- /dev/null +++ b/backend/db/queries/feeds.sql @@ -0,0 +1,32 @@ +-- name: GetFeed :one +SELECT id, url, title, fetched_at +FROM feeds +WHERE id = ?; + +-- name: GetFeeds :many +SELECT id, url, title, fetched_at +FROM feeds +ORDER BY id; + +-- name: CreateFeed :one +INSERT INTO feeds (url, title, fetched_at) +VALUES (?, ?, ?) +RETURNING *; + +-- name: UpdateFeedMetadata :exec +UPDATE feeds +SET title = ?, fetched_at = ? +WHERE id = ?; + +-- name: DeleteFeed :exec +DELETE FROM feeds +WHERE id = ?; + +-- name: GetFeedByURL :one +SELECT id, url, title, fetched_at +FROM feeds +WHERE url = ?; + +-- name: GetFeedsToFetch :many +SELECT id, url, fetched_at +FROM feeds; diff --git a/backend/db/schema.sql b/backend/db/schema.sql new file mode 100644 index 0000000..5c2bf48 --- /dev/null +++ b/backend/db/schema.sql @@ -0,0 +1,25 @@ +-- Feeds +CREATE TABLE IF NOT EXISTS feeds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT NOT NULL, + title TEXT NOT NULL, + fetched_at TEXT NOT NULL +); + +-- Articles +CREATE TABLE IF NOT EXISTS articles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + feed_id INTEGER NOT NULL, + guid TEXT NOT NULL, + title TEXT NOT NULL, + url TEXT NOT NULL, + is_read INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE +); + +-- Indice +CREATE INDEX IF NOT EXISTS idx_articles_feed_id ON articles(feed_id); + +CREATE INDEX IF NOT EXISTS idx_articles_feed_guid ON articles(feed_id, guid); + +CREATE INDEX IF NOT EXISTS idx_articles_is_read ON articles(is_read); diff --git a/backend/go.mod b/backend/go.mod index c18050e..78ca5d0 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -11,19 +11,33 @@ require ( github.com/mattn/go-sqlite3 v1.14.28 github.com/mmcdole/gofeed v1.3.0 github.com/vektah/gqlparser/v2 v2.5.30 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b ) require ( + cel.dev/expr v0.19.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/PuerkitoBio/goquery v1.10.3 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/cubicdaiya/gonp v1.0.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect + github.com/google/cel-go v0.24.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -31,13 +45,32 @@ require ( github.com/mmcdole/goxpp v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect + github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect + github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect + github.com/pingcap/log v1.1.0 // indirect + github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/riza-io/grpc-go v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sosodev/duration v1.3.1 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/sqlc-dev/sqlc v1.29.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect @@ -45,7 +78,19 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.71.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect + modernc.org/sqlite v1.37.0 // indirect ) -tool github.com/99designs/gqlgen +tool ( + github.com/99designs/gqlgen + github.com/sqlc-dev/sqlc/cmd/sqlc +) diff --git a/backend/go.sum b/backend/go.sum index 293d774..2d128a0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,10 @@ +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/99designs/gqlgen v0.17.76 h1:YsJBcfACWmXWU2t1yCjoGdOmqcTfOFpjbLAE443fmYI= github.com/99designs/gqlgen v0.17.76/go.mod h1:miiU+PkAnTIDKMQ1BseUOIVeQHoiwYDZGCswoxl7xec= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= @@ -8,20 +13,45 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= +github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -33,8 +63,27 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -54,18 +103,51 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls= +github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk= +github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE= +github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4= +github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= +github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0= +github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= +github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs= +github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -74,9 +156,40 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo= +github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -87,6 +200,7 @@ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -94,6 +208,7 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -151,6 +266,9 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -159,7 +277,51 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/gqlgen.yml b/backend/gqlgen.yml index 98f7b80..cc4472d 100644 --- a/backend/gqlgen.yml +++ b/backend/gqlgen.yml @@ -11,9 +11,9 @@ model: filename: graphql/model/generated.go resolver: - package: graphql - layout: single-file - filename: graphql/resolvers.go + package: resolver + layout: follow-schema + dir: graphql/resolver models: ID: diff --git a/backend/graphql/resolver/resolver.go b/backend/graphql/resolver/resolver.go new file mode 100644 index 0000000..7a9c389 --- /dev/null +++ b/backend/graphql/resolver/resolver.go @@ -0,0 +1,15 @@ +package resolver + +import ( + "database/sql" + "undef.ninja/x/feedaka/db" +) + +// This file will not be regenerated automatically. +// +// It serves as dependency injection for your app, add any dependencies you require here. + +type Resolver struct { + DB *sql.DB + Queries *db.Queries +} diff --git a/backend/graphql/resolvers.go b/backend/graphql/resolver/schema.resolvers.go index 3e73162..0ee771b 100644 --- a/backend/graphql/resolvers.go +++ b/backend/graphql/resolver/schema.resolvers.go @@ -1,6 +1,8 @@ -package graphql +package resolver -// THIS CODE WILL BE UPDATED WITH SCHEMA CHANGES. PREVIOUS IMPLEMENTATION FOR SCHEMA CHANGES WILL BE KEPT IN THE COMMENT SECTION. IMPLEMENTATION FOR UNCHANGED SCHEMA WILL BE KEPT. +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.76 import ( "context" @@ -10,14 +12,11 @@ import ( "time" "github.com/mmcdole/gofeed" - + "undef.ninja/x/feedaka/db" + gql "undef.ninja/x/feedaka/graphql" "undef.ninja/x/feedaka/graphql/model" ) -type Resolver struct { - DB *sql.DB -} - // AddFeed is the resolver for the addFeed field. func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed, error) { // Fetch the feed to get its title @@ -28,25 +27,24 @@ func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed } // Insert the feed into the database - result, err := r.DB.Exec( - "INSERT INTO feeds (url, title, fetched_at) VALUES (?, ?, ?)", - url, feed.Title, time.Now().UTC().Format(time.RFC3339), - ) + dbFeed, err := r.Queries.CreateFeed(ctx, db.CreateFeedParams{ + Url: url, + Title: feed.Title, + FetchedAt: time.Now().UTC().Format(time.RFC3339), + }) if err != nil { return nil, fmt.Errorf("failed to insert feed: %w", err) } - id, err := result.LastInsertId() - if err != nil { - return nil, fmt.Errorf("failed to get last insert id: %w", err) - } - // Insert articles from the feed for _, item := range feed.Items { - _, err = r.DB.Exec( - "INSERT INTO articles (feed_id, guid, title, url, is_read) VALUES (?, ?, ?, ?, ?)", - id, item.GUID, item.Title, item.Link, 0, - ) + _, err = r.Queries.CreateArticle(ctx, db.CreateArticleParams{ + FeedID: dbFeed.ID, + Guid: item.GUID, + Title: item.Title, + Url: item.Link, + IsRead: 0, + }) if err != nil { // Log but don't fail on individual article errors fmt.Printf("Failed to insert article: %v\n", err) @@ -54,10 +52,10 @@ func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed } return &model.Feed{ - ID: strconv.FormatInt(id, 10), - URL: url, - Title: feed.Title, - FetchedAt: time.Now().Format(time.RFC3339), + ID: strconv.FormatInt(dbFeed.ID, 10), + URL: dbFeed.Url, + Title: dbFeed.Title, + FetchedAt: dbFeed.FetchedAt, }, nil } @@ -75,27 +73,23 @@ func (r *mutationResolver) RemoveFeed(ctx context.Context, id string) (bool, err } defer tx.Rollback() + qtx := r.Queries.WithTx(tx) + // Delete articles first (foreign key constraint) - _, err = tx.Exec("DELETE FROM articles WHERE feed_id = ?", feedID) + err = qtx.DeleteArticlesByFeed(ctx, feedID) if err != nil { return false, fmt.Errorf("failed to delete articles: %w", err) } // Delete the feed - result, err := tx.Exec("DELETE FROM feeds WHERE id = ?", feedID) + err = qtx.DeleteFeed(ctx, feedID) if err != nil { + if err == sql.ErrNoRows { + return false, fmt.Errorf("feed not found") + } return false, fmt.Errorf("failed to delete feed: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - return false, fmt.Errorf("failed to get rows affected: %w", err) - } - - if rowsAffected == 0 { - return false, fmt.Errorf("feed not found") - } - err = tx.Commit() if err != nil { return false, fmt.Errorf("failed to commit transaction: %w", err) @@ -112,7 +106,10 @@ func (r *mutationResolver) MarkArticleRead(ctx context.Context, id string) (*mod } // Update the article's read status - _, err = r.DB.Exec("UPDATE articles SET is_read = 1 WHERE id = ?", articleID) + err = r.Queries.UpdateArticleReadStatus(ctx, db.UpdateArticleReadStatusParams{ + IsRead: 1, + ID: articleID, + }) if err != nil { return nil, fmt.Errorf("failed to mark article as read: %w", err) } @@ -129,7 +126,10 @@ func (r *mutationResolver) MarkArticleUnread(ctx context.Context, id string) (*m } // Update the article's read status - _, err = r.DB.Exec("UPDATE articles SET is_read = 0 WHERE id = ?", articleID) + err = r.Queries.UpdateArticleReadStatus(ctx, db.UpdateArticleReadStatusParams{ + IsRead: 0, + ID: articleID, + }) if err != nil { return nil, fmt.Errorf("failed to mark article as unread: %w", err) } @@ -146,7 +146,7 @@ func (r *mutationResolver) MarkFeedRead(ctx context.Context, id string) (*model. } // Update all articles in the feed to be read - _, err = r.DB.Exec("UPDATE articles SET is_read = 1 WHERE feed_id = ?", feedID) + err = r.Queries.MarkFeedArticlesRead(ctx, feedID) if err != nil { return nil, fmt.Errorf("failed to mark feed as read: %w", err) } @@ -163,7 +163,7 @@ func (r *mutationResolver) MarkFeedUnread(ctx context.Context, id string) (*mode } // Update all articles in the feed to be unread - _, err = r.DB.Exec("UPDATE articles SET is_read = 0 WHERE feed_id = ?", feedID) + err = r.Queries.MarkFeedArticlesUnread(ctx, feedID) if err != nil { return nil, fmt.Errorf("failed to mark feed as unread: %w", err) } @@ -174,24 +174,19 @@ func (r *mutationResolver) MarkFeedUnread(ctx context.Context, id string) (*mode // Feeds is the resolver for the feeds field. func (r *queryResolver) Feeds(ctx context.Context) ([]*model.Feed, error) { - rows, err := r.DB.Query("SELECT id, url, title, fetched_at FROM feeds") + dbFeeds, err := r.Queries.GetFeeds(ctx) if err != nil { return nil, fmt.Errorf("failed to query feeds: %w", err) } - defer rows.Close() var feeds []*model.Feed - for rows.Next() { - var feed model.Feed - err := rows.Scan(&feed.ID, &feed.URL, &feed.Title, &feed.FetchedAt) - if err != nil { - return nil, fmt.Errorf("failed to scan feed: %w", err) - } - feeds = append(feeds, &feed) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating over feeds: %w", err) + for _, dbFeed := range dbFeeds { + feeds = append(feeds, &model.Feed{ + ID: strconv.FormatInt(dbFeed.ID, 10), + URL: dbFeed.Url, + Title: dbFeed.Title, + FetchedAt: dbFeed.FetchedAt, + }) } return feeds, nil @@ -199,39 +194,26 @@ func (r *queryResolver) Feeds(ctx context.Context) ([]*model.Feed, error) { // UnreadArticles is the resolver for the unreadArticles field. func (r *queryResolver) UnreadArticles(ctx context.Context) ([]*model.Article, error) { - rows, err := r.DB.Query(` - SELECT a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, - f.id, f.url, f.title - FROM articles AS a - INNER JOIN feeds AS f ON a.feed_id = f.id - WHERE a.is_read = 0 - ORDER BY a.id DESC - LIMIT 100 - `) + rows, err := r.Queries.GetUnreadArticles(ctx) if err != nil { return nil, fmt.Errorf("failed to query unread articles: %w", err) } - defer rows.Close() var articles []*model.Article - for rows.Next() { - var article model.Article - var feed model.Feed - var isRead int - err := rows.Scan( - &article.ID, &article.FeedID, &article.GUID, &article.Title, &article.URL, &isRead, - &feed.ID, &feed.URL, &feed.Title, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan article: %w", err) - } - article.IsRead = isRead == 1 - article.Feed = &feed - articles = append(articles, &article) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating over articles: %w", err) + for _, row := range rows { + articles = append(articles, &model.Article{ + ID: strconv.FormatInt(row.ID, 10), + FeedID: strconv.FormatInt(row.FeedID, 10), + GUID: row.Guid, + Title: row.Title, + URL: row.Url, + IsRead: row.IsRead == 1, + Feed: &model.Feed{ + ID: strconv.FormatInt(row.FeedID2, 10), + URL: row.FeedUrl, + Title: row.FeedTitle, + }, + }) } return articles, nil @@ -239,39 +221,26 @@ func (r *queryResolver) UnreadArticles(ctx context.Context) ([]*model.Article, e // ReadArticles is the resolver for the readArticles field. func (r *queryResolver) ReadArticles(ctx context.Context) ([]*model.Article, error) { - rows, err := r.DB.Query(` - SELECT a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, - f.id, f.url, f.title - FROM articles AS a - INNER JOIN feeds AS f ON a.feed_id = f.id - WHERE a.is_read = 1 - ORDER BY a.id DESC - LIMIT 100 - `) + rows, err := r.Queries.GetReadArticles(ctx) if err != nil { return nil, fmt.Errorf("failed to query read articles: %w", err) } - defer rows.Close() var articles []*model.Article - for rows.Next() { - var article model.Article - var feed model.Feed - var isRead int - err := rows.Scan( - &article.ID, &article.FeedID, &article.GUID, &article.Title, &article.URL, &isRead, - &feed.ID, &feed.URL, &feed.Title, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan article: %w", err) - } - article.IsRead = isRead == 1 - article.Feed = &feed - articles = append(articles, &article) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating over articles: %w", err) + for _, row := range rows { + articles = append(articles, &model.Article{ + ID: strconv.FormatInt(row.ID, 10), + FeedID: strconv.FormatInt(row.FeedID, 10), + GUID: row.Guid, + Title: row.Title, + URL: row.Url, + IsRead: row.IsRead == 1, + Feed: &model.Feed{ + ID: strconv.FormatInt(row.FeedID2, 10), + URL: row.FeedUrl, + Title: row.FeedTitle, + }, + }) } return articles, nil @@ -284,11 +253,7 @@ func (r *queryResolver) Feed(ctx context.Context, id string) (*model.Feed, error return nil, fmt.Errorf("invalid feed ID: %w", err) } - var feed model.Feed - err = r.DB.QueryRow( - "SELECT id, url, title, fetched_at FROM feeds WHERE id = ?", - feedID, - ).Scan(&feed.ID, &feed.URL, &feed.Title, &feed.FetchedAt) + dbFeed, err := r.Queries.GetFeed(ctx, feedID) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("feed not found") @@ -296,7 +261,12 @@ func (r *queryResolver) Feed(ctx context.Context, id string) (*model.Feed, error return nil, fmt.Errorf("failed to query feed: %w", err) } - return &feed, nil + return &model.Feed{ + ID: strconv.FormatInt(dbFeed.ID, 10), + URL: dbFeed.Url, + Title: dbFeed.Title, + FetchedAt: dbFeed.FetchedAt, + }, nil } // Article is the resolver for the article field. @@ -306,36 +276,34 @@ func (r *queryResolver) Article(ctx context.Context, id string) (*model.Article, return nil, fmt.Errorf("invalid article ID: %w", err) } - var article model.Article - var feed model.Feed - var isRead int - err = r.DB.QueryRow(` - SELECT a.id, a.feed_id, a.guid, a.title, a.url, a.is_read, - f.id, f.url, f.title - FROM articles AS a - INNER JOIN feeds AS f ON a.feed_id = f.id - WHERE a.id = ? - `, articleID).Scan( - &article.ID, &article.FeedID, &article.GUID, &article.Title, &article.URL, &isRead, - &feed.ID, &feed.URL, &feed.Title, - ) + row, err := r.Queries.GetArticle(ctx, articleID) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("article not found") } return nil, fmt.Errorf("failed to query article: %w", err) } - article.IsRead = isRead == 1 - article.Feed = &feed - return &article, nil + return &model.Article{ + ID: strconv.FormatInt(row.ID, 10), + FeedID: strconv.FormatInt(row.FeedID, 10), + GUID: row.Guid, + Title: row.Title, + URL: row.Url, + IsRead: row.IsRead == 1, + Feed: &model.Feed{ + ID: strconv.FormatInt(row.FeedID2, 10), + URL: row.FeedUrl, + Title: row.FeedTitle, + }, + }, nil } -// Mutation returns MutationResolver implementation. -func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } +// Mutation returns gql.MutationResolver implementation. +func (r *Resolver) Mutation() gql.MutationResolver { return &mutationResolver{r} } -// Query returns QueryResolver implementation. -func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } +// Query returns gql.QueryResolver implementation. +func (r *Resolver) Query() gql.QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } diff --git a/backend/justfile b/backend/justfile index c2f97ff..7dba240 100644 --- a/backend/justfile +++ b/backend/justfile @@ -1,4 +1,4 @@ -build: +build: generate go build -o feedaka . fmt: @@ -6,3 +6,6 @@ fmt: check: go build -o /dev/null . + +generate: + go generate ./... diff --git a/backend/main.go b/backend/main.go index 90f95e3..150e0af 100644 --- a/backend/main.go +++ b/backend/main.go @@ -23,11 +23,17 @@ import ( "github.com/mmcdole/gofeed" "github.com/vektah/gqlparser/v2/ast" + "undef.ninja/x/feedaka/db" "undef.ninja/x/feedaka/graphql" + "undef.ninja/x/feedaka/graphql/resolver" ) +//go:generate go tool sqlc generate +//go:generate go tool gqlgen generate + var ( - db *sql.DB + database *sql.DB + queries *db.Queries //go:embed static/* staticFS embed.FS ) @@ -53,7 +59,7 @@ CREATE TABLE IF NOT EXISTS articles ( return err } -func fetchOneFeed(feedID int, url string, ctx context.Context) error { +func fetchOneFeed(feedID int64, url string, ctx context.Context) error { log.Printf("Fetching %s...\n", url) fp := gofeed.NewParser() ctx, cancel := context.WithTimeout(ctx, 10*time.Second) @@ -62,50 +68,41 @@ func fetchOneFeed(feedID int, url string, ctx context.Context) error { if err != nil { return fmt.Errorf("Failed to fetch %s: %v\n", url, err) } - _, err = db.Exec( - `UPDATE feeds SET title = ?, fetched_at = ? WHERE id = ?`, - feed.Title, - time.Now().UTC().Format(time.RFC3339), - feedID, - ) + err = queries.UpdateFeedMetadata(ctx, db.UpdateFeedMetadataParams{ + Title: feed.Title, + FetchedAt: time.Now().UTC().Format(time.RFC3339), + ID: feedID, + }) if err != nil { return err } - rows, err := db.Query(`SELECT guid FROM articles WHERE feed_id = ?`, feedID) + guids, err := queries.GetArticleGUIDsByFeed(ctx, feedID) if err != nil { return err } - defer rows.Close() existingArticleGUIDs := make(map[string]bool) - for rows.Next() { - var guid string - err := rows.Scan(&guid) - if err != nil { - return err - } + for _, guid := range guids { existingArticleGUIDs[guid] = true } for _, item := range feed.Items { if existingArticleGUIDs[item.GUID] { - _, err := db.Exec( - `UPDATE articles SET title = ?, url = ? WHERE feed_id = ? AND guid = ?`, - item.Title, - item.Link, - feedID, - item.GUID, - ) + err := queries.UpdateArticle(ctx, db.UpdateArticleParams{ + Title: item.Title, + Url: item.Link, + FeedID: feedID, + Guid: item.GUID, + }) if err != nil { return err } } else { - _, err := db.Exec( - `INSERT INTO articles (feed_id, guid, title, url, is_read) VALUES (?, ?, ?, ?, ?)`, - feedID, - item.GUID, - item.Title, - item.Link, - 0, - ) + _, err := queries.CreateArticle(ctx, db.CreateArticleParams{ + FeedID: feedID, + Guid: item.GUID, + Title: item.Title, + Url: item.Link, + IsRead: 0, + }) if err != nil { return err } @@ -114,23 +111,15 @@ func fetchOneFeed(feedID int, url string, ctx context.Context) error { return nil } -func listFeedsToBeFetched() (map[int]string, error) { - rows, err := db.Query(`SELECT id, url, fetched_at FROM feeds`) +func listFeedsToBeFetched(ctx context.Context) (map[int64]string, error) { + feeds, err := queries.GetFeedsToFetch(ctx) if err != nil { return nil, err } - defer rows.Close() - - feeds := make(map[int]string) - for rows.Next() { - var feedID int - var url string - var fetchedAt string - err := rows.Scan(&feedID, &url, &fetchedAt) - if err != nil { - log.Fatal(err) - } - fetchedAtTime, err := time.Parse(time.RFC3339, fetchedAt) + + result := make(map[int64]string) + for _, feed := range feeds { + fetchedAtTime, err := time.Parse(time.RFC3339, feed.FetchedAt) if err != nil { log.Fatal(err) } @@ -138,13 +127,13 @@ func listFeedsToBeFetched() (map[int]string, error) { if now.Sub(fetchedAtTime).Minutes() <= 10 { continue } - feeds[feedID] = url + result[feed.ID] = feed.Url } - return feeds, nil + return result, nil } func fetchAllFeeds(ctx context.Context) error { - feeds, err := listFeedsToBeFetched() + feeds, err := listFeedsToBeFetched(ctx) if err != nil { return err } @@ -178,17 +167,19 @@ func main() { port := os.Getenv("FEEDAKA_PORT") var err error - db, err = sql.Open("sqlite3", "feedaka.db") + database, err = sql.Open("sqlite3", "feedaka.db") if err != nil { log.Fatal(err) } - defer db.Close() + defer database.Close() - err = initDB(db) + err = initDB(database) if err != nil { log.Fatal(err) } + queries = db.New(database) + e := echo.New() e.Use(middleware.Logger()) @@ -198,7 +189,7 @@ func main() { e.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(staticFS)))) // Setup GraphQL server - srv := handler.New(graphql.NewExecutableSchema(graphql.Config{Resolvers: &graphql.Resolver{DB: db}})) + srv := handler.New(graphql.NewExecutableSchema(graphql.Config{Resolvers: &resolver.Resolver{DB: database, Queries: queries}})) srv.AddTransport(transport.Options{}) srv.AddTransport(transport.GET{}) diff --git a/backend/sqlc.yaml b/backend/sqlc.yaml new file mode 100644 index 0000000..dacc3b1 --- /dev/null +++ b/backend/sqlc.yaml @@ -0,0 +1,13 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "db/queries/" + schema: "db/schema.sql" + gen: + go: + package: "db" + out: "db" + emit_prepared_queries: false + emit_interface: false + emit_exact_table_names: false + emit_empty_slices: true |
