aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-07-12 17:11:13 +0900
committernsfisis <nsfisis@gmail.com>2025-07-12 17:52:54 +0900
commitfbe4bff7e8b6a5239c490601436fb3638dc8e13b (patch)
treeb011c43d20ebfc4566cdbe95ed878c9644797e37
parentdb4f7f4ee12ab52ff249b29496a9f0997e3dbbf5 (diff)
downloadfeedaka-fbe4bff7e8b6a5239c490601436fb3638dc8e13b.tar.gz
feedaka-fbe4bff7e8b6a5239c490601436fb3638dc8e13b.tar.zst
feedaka-fbe4bff7e8b6a5239c490601436fb3638dc8e13b.zip
feat(backend): introduce sqlc
-rw-r--r--backend/db/articles.sql.go350
-rw-r--r--backend/db/db.go31
-rw-r--r--backend/db/feeds.sql.go165
-rw-r--r--backend/db/models.go21
-rw-r--r--backend/db/queries/articles.sql73
-rw-r--r--backend/db/queries/feeds.sql32
-rw-r--r--backend/db/schema.sql25
-rw-r--r--backend/go.mod49
-rw-r--r--backend/go.sum166
-rw-r--r--backend/gqlgen.yml6
-rw-r--r--backend/graphql/resolver/resolver.go15
-rw-r--r--backend/graphql/resolver/schema.resolvers.go (renamed from backend/graphql/resolvers.go)236
-rw-r--r--backend/justfile5
-rw-r--r--backend/main.go95
-rw-r--r--backend/sqlc.yaml13
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