From ebaec3d29693a3b7c8cb56410d76f81cec5685c2 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 19 Feb 2026 21:21:27 +1100 Subject: [PATCH] Modified models to support a DB --- .gitignore | 1 + Makefile | 11 +++ go.mod | 18 ++++ go.sum | 31 +++++++ {site_templates => layouts/simplecss}/fs.go | 2 +- .../simplecss}/layout_main.html | 0 .../simplecss}/posts_list.html | 0 .../simplecss}/posts_single.html | 0 main.go | 13 ++- models/posts.go | 14 ++++ models/pubmodel/sites.go | 9 ++ models/sites.go | 24 +++++- models/users.go | 7 ++ providers/db/gen/sql/db.go | 31 +++++++ providers/db/gen/sql/models.go | 38 +++++++++ providers/db/gen/sql/posts.sql.go | 84 +++++++++++++++++++ providers/db/gen/sql/pubtargets.sql.go | 76 +++++++++++++++++ providers/db/gen/sql/sites.sql.go | 64 ++++++++++++++ providers/db/gen/sql/users.sql.go | 37 ++++++++ providers/db/provider.go | 40 +++++++++ providers/sitebuilder/builder.go | 21 ++--- providers/sitebuilder/builder_test.go | 25 +++--- providers/sitebuilder/tmplfns.go | 8 +- providers/sitebuilder/tmpls.go | 7 +- providers/sitereader/models.go | 27 ++++++ providers/sitereader/provider.go | 42 +++++++--- providers/sitereader/provider_test.go | 21 +++-- sql/queries/posts.sql | 14 ++++ sql/queries/pubtargets.sql | 12 +++ sql/queries/sites.sql | 10 +++ sql/queries/users.sql | 5 ++ sql/schema/01_init.up.sql | 38 +++++++++ sqlc.yaml | 9 ++ 33 files changed, 675 insertions(+), 64 deletions(-) create mode 100644 Makefile rename {site_templates => layouts/simplecss}/fs.go (68%) rename {site_templates => layouts/simplecss}/layout_main.html (100%) rename {site_templates => layouts/simplecss}/posts_list.html (100%) rename {site_templates => layouts/simplecss}/posts_single.html (100%) create mode 100644 models/posts.go create mode 100644 models/pubmodel/sites.go create mode 100644 models/users.go create mode 100644 providers/db/gen/sql/db.go create mode 100644 providers/db/gen/sql/models.go create mode 100644 providers/db/gen/sql/posts.sql.go create mode 100644 providers/db/gen/sql/pubtargets.sql.go create mode 100644 providers/db/gen/sql/sites.sql.go create mode 100644 providers/db/gen/sql/users.sql.go create mode 100644 providers/db/provider.go create mode 100644 providers/sitereader/models.go create mode 100644 sql/queries/posts.sql create mode 100644 sql/queries/pubtargets.sql create mode 100644 sql/queries/sites.sql create mode 100644 sql/queries/users.sql create mode 100644 sql/schema/01_init.up.sql create mode 100644 sqlc.yaml diff --git a/.gitignore b/.gitignore index 3974640..3703748 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ +.idea/ # Local Netlify folder .netlify diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b1af52 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +BUILD_DIR=build + +all: clean build + +.Phony: clean +clean: + -rm -r $(BUILD_DIR) + +.Phony: gen +gen: + go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.30.0 generate \ No newline at end of file diff --git a/go.mod b/go.mod index 0465f3a..3c8f82e 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,28 @@ module lmika.dev/lmika/weiro go 1.24.3 +require ( + github.com/Southclaws/fault v0.8.1 + github.com/lmika/blogging-tools v0.0.0-20240630114557-8db2b3aa93e6 + lmika.dev/pkg/litemigrate v0.1.0 +) + require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/yuin/goldmark v1.7.16 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/sys v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.46.1 // indirect ) diff --git a/go.sum b/go.sum index 7e80bf7..84ff803 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,42 @@ +github.com/Southclaws/fault v0.8.1 h1:mgqqdC6kUBQ6ExMALZ0nNaDfNJD5h2+wq3se5mAyX+8= +github.com/Southclaws/fault v0.8.1/go.mod h1:VUVkAWutC59SL16s6FTqf3I6I2z77RmnaW5XRz4bLOE= 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/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/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/lmika/blogging-tools v0.0.0-20240630114557-8db2b3aa93e6 h1:WHM70gRzPTKHSKm/wf2kp3HErXzMpmcqfkGjHlhtu1Y= +github.com/lmika/blogging-tools v0.0.0-20240630114557-8db2b3aa93e6/go.mod h1:w4rGqiE0+/FDUNWiIhfPGSPfT948/9Yw+cja12zOb4o= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lmika.dev/pkg/litemigrate v0.1.0 h1:DBEJahbQO7W3uEmAOQGg1URBWYimg0ClWHi83M2MZwk= +lmika.dev/pkg/litemigrate v0.1.0/go.mod h1:GQWWDiMZGQaVspcwKNq8vIBPN5H+KsUo/VBIeh9OfLg= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= diff --git a/site_templates/fs.go b/layouts/simplecss/fs.go similarity index 68% rename from site_templates/fs.go rename to layouts/simplecss/fs.go index 461cea4..2c1b2fb 100644 --- a/site_templates/fs.go +++ b/layouts/simplecss/fs.go @@ -1,4 +1,4 @@ -package site_templates +package simplecss import "embed" diff --git a/site_templates/layout_main.html b/layouts/simplecss/layout_main.html similarity index 100% rename from site_templates/layout_main.html rename to layouts/simplecss/layout_main.html diff --git a/site_templates/posts_list.html b/layouts/simplecss/posts_list.html similarity index 100% rename from site_templates/posts_list.html rename to layouts/simplecss/posts_list.html diff --git a/site_templates/posts_single.html b/layouts/simplecss/posts_single.html similarity index 100% rename from site_templates/posts_single.html rename to layouts/simplecss/posts_single.html diff --git a/main.go b/main.go index e1bbbf3..cef3dff 100644 --- a/main.go +++ b/main.go @@ -4,22 +4,29 @@ import ( "log" "os" + "lmika.dev/lmika/weiro/layouts/simplecss" + "lmika.dev/lmika/weiro/models/pubmodel" "lmika.dev/lmika/weiro/providers/sitebuilder" "lmika.dev/lmika/weiro/providers/sitereader" - "lmika.dev/lmika/weiro/site_templates" ) func main() { sr := sitereader.New(os.DirFS("_test-site")) - site, err := sr.ReadSite() + readSite, err := sr.ReadSite() if err != nil { log.Fatal(err) } + site := pubmodel.Site{ + Site: readSite.Site, + BaseURL: readSite.Target.BaseURL, + Posts: readSite.Posts, + } + sb, err := sitebuilder.New(site, sitebuilder.Options{ BasePosts: "/posts", - TemplatesFS: site_templates.FS, + TemplatesFS: simplecss.FS, }) if err != nil { log.Fatal(err) diff --git a/models/posts.go b/models/posts.go new file mode 100644 index 0000000..9d85e6f --- /dev/null +++ b/models/posts.go @@ -0,0 +1,14 @@ +package models + +import "time" + +type Post struct { + ID int64 + SiteID int64 + GUID string + Title string + Body string + Slug string + CreatedAt time.Time + PublishedAt time.Time +} diff --git a/models/pubmodel/sites.go b/models/pubmodel/sites.go new file mode 100644 index 0000000..74e7444 --- /dev/null +++ b/models/pubmodel/sites.go @@ -0,0 +1,9 @@ +package pubmodel + +import "lmika.dev/lmika/weiro/models" + +type Site struct { + models.Site + BaseURL string + Posts []*models.Post +} diff --git a/models/sites.go b/models/sites.go index fb72648..dae3a2e 100644 --- a/models/sites.go +++ b/models/sites.go @@ -1,12 +1,29 @@ package models -import "time" +const ( + PublishTargetTypeNone int = iota + PublishTargetTypeNetlify +) type Site struct { - Meta SiteMeta - Posts []*Post + ID int64 + OwnerID int64 + Title string + Tagline string + //Meta SiteMeta + //Posts []*Post } +type SitePublishTarget struct { + ID int64 + SiteID int64 + PublishTargetType int + BaseURL string + TargetSiteID string + TargetPublishKey string +} + +/* type SiteMeta struct { Title string `yaml:"title"` Tagline string `yaml:"tagline"` @@ -25,3 +42,4 @@ type Post struct { Meta PostMeta Content string } +*/ diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..13c6452 --- /dev/null +++ b/models/users.go @@ -0,0 +1,7 @@ +package models + +type User struct { + ID int64 + Username string + PasswordHashed []byte +} diff --git a/providers/db/gen/sql/db.go b/providers/db/gen/sql/db.go new file mode 100644 index 0000000..13a2738 --- /dev/null +++ b/providers/db/gen/sql/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package sql + +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/providers/db/gen/sql/models.go b/providers/db/gen/sql/models.go new file mode 100644 index 0000000..b7d7996 --- /dev/null +++ b/providers/db/gen/sql/models.go @@ -0,0 +1,38 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package sql + +type Post struct { + ID int64 + SiteID int64 + Guid string + Title string + Body string + Slug string + CreatedAt int64 + PublishedAt int64 +} + +type PublishTarget struct { + ID int64 + SiteID int64 + PublishTargetType int64 + BaseUrl string + TargetSiteID string + TargetPublishKey string +} + +type Site struct { + ID int64 + OwnerID int64 + Title string + Tagline string +} + +type User struct { + ID int64 + Username string + Password string +} diff --git a/providers/db/gen/sql/posts.sql.go b/providers/db/gen/sql/posts.sql.go new file mode 100644 index 0000000..8cf5905 --- /dev/null +++ b/providers/db/gen/sql/posts.sql.go @@ -0,0 +1,84 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: posts.sql + +package sql + +import ( + "context" +) + +const insertPost = `-- name: InsertPost :one +INSERT INTO posts ( + site_id, + guid, + title, + body, + slug, + created_at, + published_at +) VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING id +` + +type InsertPostParams struct { + SiteID int64 + Guid string + Title string + Body string + Slug string + CreatedAt int64 + PublishedAt int64 +} + +func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPost, + arg.SiteID, + arg.Guid, + arg.Title, + arg.Body, + arg.Slug, + arg.CreatedAt, + arg.PublishedAt, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectPostsOfSite = `-- name: SelectPostsOfSite :many +SELECT id, site_id, guid, title, body, slug, created_at, published_at FROM posts WHERE site_id = ? ORDER BY created_at DESC LIMIT 10 +` + +func (q *Queries) SelectPostsOfSite(ctx context.Context, siteID int64) ([]Post, error) { + rows, err := q.db.QueryContext(ctx, selectPostsOfSite, siteID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Post + for rows.Next() { + var i Post + if err := rows.Scan( + &i.ID, + &i.SiteID, + &i.Guid, + &i.Title, + &i.Body, + &i.Slug, + &i.CreatedAt, + &i.PublishedAt, + ); 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 +} diff --git a/providers/db/gen/sql/pubtargets.sql.go b/providers/db/gen/sql/pubtargets.sql.go new file mode 100644 index 0000000..a8898a1 --- /dev/null +++ b/providers/db/gen/sql/pubtargets.sql.go @@ -0,0 +1,76 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: pubtargets.sql + +package sql + +import ( + "context" +) + +const insertPublishTarget = `-- name: InsertPublishTarget :one +INSERT INTO publish_targets ( + site_id, + publish_target_type, + base_url, + target_site_id, + target_publish_key +) VALUES (?, ?, ?, ?, ?) +RETURNING id +` + +type InsertPublishTargetParams struct { + SiteID int64 + PublishTargetType int64 + BaseUrl string + TargetSiteID string + TargetPublishKey string +} + +func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPublishTarget, + arg.SiteID, + arg.PublishTargetType, + arg.BaseUrl, + arg.TargetSiteID, + arg.TargetPublishKey, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectPublishTargetsOfSite = `-- name: SelectPublishTargetsOfSite :many +SELECT id, site_id, publish_target_type, base_url, target_site_id, target_publish_key FROM publish_targets WHERE site_id = ? +` + +func (q *Queries) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) ([]PublishTarget, error) { + rows, err := q.db.QueryContext(ctx, selectPublishTargetsOfSite, siteID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PublishTarget + for rows.Next() { + var i PublishTarget + if err := rows.Scan( + &i.ID, + &i.SiteID, + &i.PublishTargetType, + &i.BaseUrl, + &i.TargetSiteID, + &i.TargetPublishKey, + ); 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 +} diff --git a/providers/db/gen/sql/sites.sql.go b/providers/db/gen/sql/sites.sql.go new file mode 100644 index 0000000..799e8fc --- /dev/null +++ b/providers/db/gen/sql/sites.sql.go @@ -0,0 +1,64 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: sites.sql + +package sql + +import ( + "context" +) + +const insertSite = `-- name: InsertSite :one +INSERT INTO sites ( + owner_id, + title, + tagline +) VALUES (?, ?, ?) +RETURNING id +` + +type InsertSiteParams struct { + OwnerID int64 + Title string + Tagline string +} + +func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertSite, arg.OwnerID, arg.Title, arg.Tagline) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectSitesOwnedByUser = `-- name: SelectSitesOwnedByUser :many +SELECT id, owner_id, title, tagline FROM sites WHERE owner_id = ? +` + +func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]Site, error) { + rows, err := q.db.QueryContext(ctx, selectSitesOwnedByUser, ownerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Site + for rows.Next() { + var i Site + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.Title, + &i.Tagline, + ); 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 +} diff --git a/providers/db/gen/sql/users.sql.go b/providers/db/gen/sql/users.sql.go new file mode 100644 index 0000000..2c07fe8 --- /dev/null +++ b/providers/db/gen/sql/users.sql.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: users.sql + +package sql + +import ( + "context" +) + +const insertUserByUsername = `-- name: InsertUserByUsername :one +INSERT INTO users (username, password) VALUES (?, ?) RETURNING id +` + +type InsertUserByUsernameParams struct { + Username string + Password string +} + +func (q *Queries) InsertUserByUsername(ctx context.Context, arg InsertUserByUsernameParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertUserByUsername, arg.Username, arg.Password) + var id int64 + err := row.Scan(&id) + return id, err +} + +const selectUserByUsername = `-- name: SelectUserByUsername :one +SELECT id, username, password FROM users WHERE username = ? +` + +func (q *Queries) SelectUserByUsername(ctx context.Context, username string) (User, error) { + row := q.db.QueryRowContext(ctx, selectUserByUsername, username) + var i User + err := row.Scan(&i.ID, &i.Username, &i.Password) + return i, err +} diff --git a/providers/db/provider.go b/providers/db/provider.go new file mode 100644 index 0000000..d33f0ed --- /dev/null +++ b/providers/db/provider.go @@ -0,0 +1,40 @@ +package db + +import ( + "context" + "database/sql" + + "github.com/Southclaws/fault" + "github.com/lmika/blogging-tools/providers/db/sqlc/maindbq" + "github.com/lmika/blogging-tools/sql/maindb/schema" + migration "lmika.dev/pkg/litemigrate" +) + +type Provider struct { + drvr *sql.DB + queries *maindbq.Queries +} + +func New(dbFile string) (*Provider, error) { + drvr, err := sql.Open("sqlite", dbFile) + if err != nil { + return nil, fault.Wrap(err) + } + + if err := migration.New(schema.FS, drvr).MigrateUp(context.Background()); err != nil { + return nil, fault.Wrap(err) + } + + if _, err := drvr.Exec(`PRAGMA foreign_keys = 1;`); err != nil { + return nil, fault.Wrap(err) + } + + return &Provider{ + drvr: drvr, + queries: maindbq.New(drvr), + }, nil +} + +func (db *Provider) Close() error { + return db.drvr.Close() +} diff --git a/providers/sitebuilder/builder.go b/providers/sitebuilder/builder.go index a76401e..cba9693 100644 --- a/providers/sitebuilder/builder.go +++ b/providers/sitebuilder/builder.go @@ -16,16 +16,17 @@ import ( "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer/html" "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/models/pubmodel" ) type Builder struct { - site models.Site + site pubmodel.Site gmMarkdown goldmark.Markdown opts Options tmpls *template.Template } -func New(site models.Site, opts Options) (*Builder, error) { +func New(site pubmodel.Site, opts Options) (*Builder, error) { tmpls, err := template.New(""). Funcs(templateFns(site, opts)). ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain) @@ -75,11 +76,11 @@ func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) erro copy(postCopy, postList) sort.Slice(postCopy, func(i, j int) bool { - return postCopy[i].Meta.Date.After(postCopy[j].Meta.Date) + return postCopy[i].PublishedAt.After(postCopy[j].PublishedAt) }) pl := postListData{ - commonData: commonData{Site: b.site.Meta}, + commonData: commonData{Site: b.site}, } for _, post := range postCopy { rp, err := b.renderPost(post) @@ -95,20 +96,20 @@ func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) erro } func (b *Builder) renderPost(post *models.Post) (postSingleData, error) { - postPath := post.Meta.Slug + postPath := post.Slug if b.opts.BasePosts != "" { postPath = filepath.Join(b.opts.BasePosts, strings.TrimPrefix(postPath, "/")) } var md bytes.Buffer - if err := b.gmMarkdown.Convert([]byte(post.Content), &md); err != nil { - return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Meta.Slug, err) + if err := b.gmMarkdown.Convert([]byte(post.Body), &md); err != nil { + return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err) } return postSingleData{ - commonData: commonData{Site: b.site.Meta}, + commonData: commonData{Site: b.site}, Path: postPath, - Meta: post.Meta, + Meta: post, HTML: template.HTML(md.String()), }, nil } @@ -152,7 +153,7 @@ func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) err } return b.tmpls.ExecuteTemplate(w, tmplNameLayoutMain, layoutData{ - commonData: commonData{Site: b.site.Meta}, + commonData: commonData{Site: b.site}, Body: template.HTML(buf.String()), }) } diff --git a/providers/sitebuilder/builder_test.go b/providers/sitebuilder/builder_test.go index d92c713..249b5f8 100644 --- a/providers/sitebuilder/builder_test.go +++ b/providers/sitebuilder/builder_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/models/pubmodel" "lmika.dev/lmika/weiro/providers/sitebuilder" ) @@ -19,24 +20,18 @@ func TestBuilder_BuildSite(t *testing.T) { "layout_main.html": {Data: []byte(`{{ .Body }}`)}, } - site := models.Site{ - Meta: models.SiteMeta{ - BaseURL: "https://example.com", - }, + site := pubmodel.Site{ + BaseURL: "https://example.com", Posts: []*models.Post{ { - Meta: models.PostMeta{ - Title: "Test Post", - Slug: "/2026/02/18/test-post", - }, - Content: "This is a test post", + Title: "Test Post", + Slug: "/2026/02/18/test-post", + Body: "This is a test post", }, { - Meta: models.PostMeta{ - Title: "Another Post", - Slug: "/2026/02/20/another-post", - }, - Content: "This is **another** test post", + Title: "Another Post", + Slug: "/2026/02/20/another-post", + Body: "This is **another** test post", }, }, } @@ -64,4 +59,4 @@ func TestBuilder_BuildSite(t *testing.T) { } }) -} +} \ No newline at end of file diff --git a/providers/sitebuilder/tmplfns.go b/providers/sitebuilder/tmplfns.go index abbf60d..4b69a57 100644 --- a/providers/sitebuilder/tmplfns.go +++ b/providers/sitebuilder/tmplfns.go @@ -6,17 +6,17 @@ import ( "path/filepath" "time" - "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/models/pubmodel" ) -func templateFns(site models.Site, opts Options) template.FuncMap { +func templateFns(site pubmodel.Site, opts Options) template.FuncMap { return template.FuncMap{ "url_abs": func(basePath string) (string, error) { - if site.Meta.BaseURL == "" { + if site.BaseURL == "" { return basePath, nil } - pu, err := url.Parse(site.Meta.BaseURL) + pu, err := url.Parse(site.BaseURL) if err != nil { return "", err } diff --git a/providers/sitebuilder/tmpls.go b/providers/sitebuilder/tmpls.go index 9fe2507..b9a098a 100644 --- a/providers/sitebuilder/tmpls.go +++ b/providers/sitebuilder/tmpls.go @@ -6,6 +6,7 @@ import ( "time" "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/models/pubmodel" ) const ( @@ -22,8 +23,6 @@ const ( ) type Options struct { - SiteMeta models.SiteMeta - // BasePosts is the base path for posts. BasePosts string @@ -34,12 +33,12 @@ type Options struct { } type commonData struct { - Site models.SiteMeta + Site pubmodel.Site } type postSingleData struct { commonData - Meta models.PostMeta + Meta *models.Post HTML template.HTML Path string } diff --git a/providers/sitereader/models.go b/providers/sitereader/models.go new file mode 100644 index 0000000..276576d --- /dev/null +++ b/providers/sitereader/models.go @@ -0,0 +1,27 @@ +package sitereader + +import ( + "time" + + "lmika.dev/lmika/weiro/models" +) + +type ReadSiteModels struct { + Site models.Site + Target models.SitePublishTarget + Posts []*models.Post +} + +type siteMeta struct { + Title string `yaml:"title"` + Tagline string `yaml:"tagline"` + BaseURL string `yaml:"base_url"` +} + +type postMeta struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Date time.Time `yaml:"date"` + Tags []string `yaml:"tags"` + Slug string `yaml:"slug"` +} diff --git a/providers/sitereader/provider.go b/providers/sitereader/provider.go index c51e886..c911d04 100644 --- a/providers/sitereader/provider.go +++ b/providers/sitereader/provider.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "io/fs" + "time" "gopkg.in/yaml.v3" "lmika.dev/lmika/weiro/models" @@ -19,24 +20,33 @@ func New(fs fs.FS) *Provider { } } -func (p *Provider) ReadSite() (models.Site, error) { +func (p *Provider) ReadSite() (ReadSiteModels, error) { posts, err := p.ListPosts() if err != nil { - return models.Site{}, err + return ReadSiteModels{}, err } - meta := models.SiteMeta{} + meta := siteMeta{} metaBytes, err := fs.ReadFile(p.fs, "site.yaml") if err != nil { - return models.Site{}, err + return ReadSiteModels{}, err } if err := yaml.Unmarshal(metaBytes, &meta); err != nil { - return models.Site{}, err + return ReadSiteModels{}, err } - return models.Site{ - Meta: meta, - Posts: posts, + site := models.Site{ + Title: meta.Title, + Tagline: meta.Tagline, + } + publishTarget := models.SitePublishTarget{ + BaseURL: meta.BaseURL, + } + + return ReadSiteModels{ + Site: site, + Target: publishTarget, + Posts: posts, }, nil } @@ -70,13 +80,19 @@ func (p *Provider) ReadPost(path string) (*models.Post, error) { return nil, io.ErrUnexpectedEOF } - var meta models.PostMeta + var meta postMeta if err := yaml.Unmarshal(parts[1], &meta); err != nil { return nil, err } - return &models.Post{ - Meta: meta, - Content: string(bytes.TrimPrefix(parts[2], []byte("\n"))), - }, nil + post := models.Post{ + Slug: meta.Slug, + Title: meta.Title, + GUID: meta.ID, + PublishedAt: meta.Date, + CreatedAt: time.Now(), + } + + post.Body = string(bytes.TrimPrefix(parts[2], []byte("\n"))) + return &post, nil } diff --git a/providers/sitereader/provider_test.go b/providers/sitereader/provider_test.go index 0986b9a..0b012eb 100644 --- a/providers/sitereader/provider_test.go +++ b/providers/sitereader/provider_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "lmika.dev/lmika/weiro/models" "lmika.dev/lmika/weiro/providers/sitereader" ) @@ -27,10 +26,9 @@ This is just a test post. post, err := pr.ReadPost("posts/test.md") assert.NoError(t, err) - assert.Equal(t, "Test Post Here", post.Meta.Title) - assert.Equal(t, time.Date(2026, 2, 18, 19, 59, 0, 0, time.UTC), post.Meta.Date) - assert.Equal(t, []string{"test", "example"}, post.Meta.Tags) - assert.Equal(t, "This is just a test post.\n", post.Content) + assert.Equal(t, "Test Post Here", post.Title) + assert.Equal(t, time.Date(2026, 2, 18, 19, 59, 0, 0, time.UTC), post.PublishedAt) + assert.Equal(t, "This is just a test post.\n", post.Body) }) t.Run("without meta", func(t *testing.T) { @@ -45,8 +43,8 @@ This is just a test post. post, err := pr.ReadPost("posts/test.md") assert.NoError(t, err) - assert.Equal(t, models.PostMeta{}, post.Meta) - assert.Equal(t, "This is just a test post.\n", post.Content) + assert.Equal(t, "", post.Title) + assert.Equal(t, "This is just a test post.\n", post.Body) }) } @@ -74,12 +72,13 @@ This is just a test post. assert.Equal(t, 2, len(posts)) - assert.Equal(t, "111", posts[0].Meta.ID) - assert.Equal(t, "222", posts[1].Meta.ID) + assert.Equal(t, "111", posts[0].GUID) + assert.Equal(t, "222", posts[1].GUID) } func TestProvider_ReadSite(t *testing.T) { testFS := fstest.MapFS{ + "site.yaml": {Data: []byte(`base_url: https://example.com`)}, "posts/01-post1.md": {Data: []byte(`--- id: 111 date: 2026-02-18T19:59:00Z @@ -102,6 +101,6 @@ This is just a test post. assert.Equal(t, 2, len(sites.Posts)) - assert.Equal(t, "111", sites.Posts[0].Meta.ID) - assert.Equal(t, "222", sites.Posts[1].Meta.ID) + assert.Equal(t, "111", sites.Posts[0].GUID) + assert.Equal(t, "222", sites.Posts[1].GUID) } diff --git a/sql/queries/posts.sql b/sql/queries/posts.sql new file mode 100644 index 0000000..b1644be --- /dev/null +++ b/sql/queries/posts.sql @@ -0,0 +1,14 @@ +-- name: SelectPostsOfSite :many +SELECT * FROM posts WHERE site_id = ? ORDER BY created_at DESC LIMIT 10; + +-- name: InsertPost :one +INSERT INTO posts ( + site_id, + guid, + title, + body, + slug, + created_at, + published_at +) VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING id; \ No newline at end of file diff --git a/sql/queries/pubtargets.sql b/sql/queries/pubtargets.sql new file mode 100644 index 0000000..d0f39c6 --- /dev/null +++ b/sql/queries/pubtargets.sql @@ -0,0 +1,12 @@ +-- name: SelectPublishTargetsOfSite :many +SELECT * FROM publish_targets WHERE site_id = ?; + +-- name: InsertPublishTarget :one +INSERT INTO publish_targets ( + site_id, + publish_target_type, + base_url, + target_site_id, + target_publish_key +) VALUES (?, ?, ?, ?, ?) +RETURNING id; \ No newline at end of file diff --git a/sql/queries/sites.sql b/sql/queries/sites.sql new file mode 100644 index 0000000..e751b82 --- /dev/null +++ b/sql/queries/sites.sql @@ -0,0 +1,10 @@ +-- name: SelectSitesOwnedByUser :many +SELECT * FROM sites WHERE owner_id = ?; + +-- name: InsertSite :one +INSERT INTO sites ( + owner_id, + title, + tagline +) VALUES (?, ?, ?) +RETURNING id; \ No newline at end of file diff --git a/sql/queries/users.sql b/sql/queries/users.sql new file mode 100644 index 0000000..547b516 --- /dev/null +++ b/sql/queries/users.sql @@ -0,0 +1,5 @@ +-- name: SelectUserByUsername :one +SELECT * FROM users WHERE username = ?; + +-- name: InsertUserByUsername :one +INSERT INTO users (username, password) VALUES (?, ?) RETURNING id; \ No newline at end of file diff --git a/sql/schema/01_init.up.sql b/sql/schema/01_init.up.sql new file mode 100644 index 0000000..282c77d --- /dev/null +++ b/sql/schema/01_init.up.sql @@ -0,0 +1,38 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password TEXT NOT NULL +); +CREATE UNIQUE INDEX idx_users_username ON users (username); + +CREATE TABLE sites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner_id INTEGER NOT NULL, + title TEXT NOT NULL, + tagline TEXT NOT NULL, + + FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE +); +CREATE INDEX idx_site_owner ON site (owner_id); + +CREATE TABLE publish_targets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id INTEGER NOT NULL, + publish_target_type INTEGER NOT NULL, + base_url TEXT NOT NULL, + target_site_id TEXT NOT NULL, + target_publish_key TEXT NOT NULL +); +CREATE INDEX idx_publish_targets_site ON publish_targets (site_id); + +CREATE TABLE posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id INTEGER NOT NULL, + guid TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + slug TEXT NOT NULL, + created_at INTEGER NOT NULL, + published_at INTEGER NOT NULL +); +CREATE INDEX idx_post_site ON posts (site_id); \ No newline at end of file diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..e0f2272 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "sql/queries" + schema: "sql/schema" + gen: + go: + package: "sql" + out: "providers/db/gen/sql" \ No newline at end of file