diff --git a/_test-site/posts/2026/02/19-about-a-db.md b/_test-site/posts/2026/02/19-about-a-db.md new file mode 100644 index 0000000..7e386a6 --- /dev/null +++ b/_test-site/posts/2026/02/19-about-a-db.md @@ -0,0 +1,18 @@ +--- +date: 2026-02-19T22:17:00+11:00 +title: About a DB +slug: /2026/02/19/about-a-db +--- +Hello again. + +This is the second post with Weiro. This evening, I wondered how best to manage the storage of these posts. +I know that a few developers have opted in for files on a file system. It's an interesting approach, and probably +a good one for most. But I couldn't get my head around effective sorting and filtering. Having a largish blog, +say 3000 posts or so, would be difficult to leave up to the file system alone, especially when it comes to +paging and searching in the posts list. + +So I opted for a database. I'm using a [Go port of sqlite3](https://pkg.go.dev/modernc.org/sqlite). +with [SQLC](https://github.com/kyleconroy/sqlc) to generate the queries. There's still no UI, so what the current +version of Weiro does is import the site from Markdown files, load it into the database, then reads it from the +database and publishes it to the file system. I was able to repurpose the existing code as an import service, making +it potentially quite useful even when the bulk of the post is managed via the frontend. \ No newline at end of file diff --git a/go.mod b/go.mod index 3c8f82e..37bdb03 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,13 @@ module lmika.dev/lmika/weiro go 1.24.3 require ( + emperror.dev/errors v0.8.1 github.com/Southclaws/fault v0.8.1 - github.com/lmika/blogging-tools v0.0.0-20240630114557-8db2b3aa93e6 + github.com/stretchr/testify v1.11.1 + github.com/yuin/goldmark v1.7.16 + gopkg.in/yaml.v3 v3.0.1 lmika.dev/pkg/litemigrate v0.1.0 + modernc.org/sqlite v1.46.1 ) require ( @@ -15,15 +19,14 @@ require ( 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/pkg/errors v0.9.1 // 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 + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // 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 84ff803..c097703 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,18 @@ +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= 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.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/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/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/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/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= @@ -14,29 +21,64 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.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/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.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= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 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/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 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/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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +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/layouts/simplecss/posts_list.html b/layouts/simplecss/posts_list.html index 0eb2496..944c5a1 100644 --- a/layouts/simplecss/posts_list.html +++ b/layouts/simplecss/posts_list.html @@ -1,5 +1,5 @@ {{ range .Posts }} - {{ if .Meta.Title }}

{{ .Meta.Title }}

{{ end }} + {{ if .Post.Title }}

{{ .Post.Title }}

{{ end }} {{ .HTML }} - {{ format_date .Meta.Date }} + {{ format_date .Post.PublishedAt }} {{ end }} \ No newline at end of file diff --git a/layouts/simplecss/posts_single.html b/layouts/simplecss/posts_single.html index 2a26c30..5fd9fcb 100644 --- a/layouts/simplecss/posts_single.html +++ b/layouts/simplecss/posts_single.html @@ -1,3 +1,3 @@ -{{ if .Meta.Title }}

{{ .Meta.Title }}

{{ end }} +{{ if .Post.Title }}

{{ .Post.Title }}

{{ end }} {{ .HTML }} -{{ format_date .Meta.Date }} \ No newline at end of file +{{ format_date .Post.PublishedAt }} \ No newline at end of file diff --git a/main.go b/main.go index cef3dff..619255d 100644 --- a/main.go +++ b/main.go @@ -1,38 +1,57 @@ package main import ( + "context" "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/models" + "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/lmika/weiro/services/importer" + "lmika.dev/lmika/weiro/services/publisher" + + _ "modernc.org/sqlite" ) func main() { - sr := sitereader.New(os.DirFS("_test-site")) + dbp, err := db.New("build/weiro.db") + if err != nil { + log.Fatal(err) + } + defer dbp.Close() - readSite, err := sr.ReadSite() + user, err := dbp.SelectUserByUsername(context.Background(), "testuser") + if err != nil { + user = models.User{ + Username: "testuser", + PasswordHashed: []byte("changeme"), + } + if err := dbp.SaveUser(context.Background(), &user); err != nil { + log.Fatal(err) + } + } + + importerSvc := importer.New(dbp) + publisherSvc := publisher.New(dbp) + + ctx := models.WithUser(context.Background(), user) + + site, err := importerSvc.Import(ctx, "_test-site") if err != nil { log.Fatal(err) } - site := pubmodel.Site{ - Site: readSite.Site, - BaseURL: readSite.Target.BaseURL, - Posts: readSite.Posts, + target := models.SitePublishTarget{ + SiteID: site.ID, + BaseURL: "https://jolly-boba-9e2486.netlify.app", + TargetType: models.PublishTargetTypeLocalFS, + TargetRef: "build/out", + TargetKey: "", } - - sb, err := sitebuilder.New(site, sitebuilder.Options{ - BasePosts: "/posts", - TemplatesFS: simplecss.FS, - }) - if err != nil { + if err := dbp.SavePublishTarget(ctx, &target); err != nil { log.Fatal(err) } - if err := sb.BuildSite("build/out"); err != nil { + if err := publisherSvc.Publish(ctx, site.ID); err != nil { log.Fatal(err) } diff --git a/models/ctx.go b/models/ctx.go new file mode 100644 index 0000000..9ffe664 --- /dev/null +++ b/models/ctx.go @@ -0,0 +1,16 @@ +package models + +import "context" + +type userKeyType struct{} + +var userKey = userKeyType{} + +func WithUser(ctx context.Context, user User) context.Context { + return context.WithValue(ctx, userKey, user) +} + +func GetUser(ctx context.Context) (User, bool) { + user, ok := ctx.Value(userKey).(User) + return user, ok +} diff --git a/models/errors.go b/models/errors.go new file mode 100644 index 0000000..9b2f3c5 --- /dev/null +++ b/models/errors.go @@ -0,0 +1,6 @@ +package models + +import "emperror.dev/errors" + +var UserRequiredError = errors.New("user required") +var PermissionError = errors.New("permission denied") diff --git a/models/sites.go b/models/sites.go index dae3a2e..96ca7ab 100644 --- a/models/sites.go +++ b/models/sites.go @@ -1,8 +1,11 @@ package models +type PublishTargetType int + const ( - PublishTargetTypeNone int = iota - PublishTargetTypeNetlify + PublishTargetTypeNone PublishTargetType = 0 + PublishTargetTypeLocalFS PublishTargetType = 1 + PublishTargetTypeNetlify PublishTargetType = 2 ) type Site struct { @@ -15,12 +18,13 @@ type Site struct { } type SitePublishTarget struct { - ID int64 - SiteID int64 - PublishTargetType int - BaseURL string - TargetSiteID string - TargetPublishKey string + ID int64 + SiteID int64 + + BaseURL string + TargetType PublishTargetType + TargetRef string + TargetKey string } /* diff --git a/providers/db/gen/sql/users.sql.go b/providers/db/gen/sql/users.sql.go deleted file mode 100644 index 2c07fe8..0000000 --- a/providers/db/gen/sql/users.sql.go +++ /dev/null @@ -1,37 +0,0 @@ -// 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/gen/sql/db.go b/providers/db/gen/sqlgen/db.go similarity index 94% rename from providers/db/gen/sql/db.go rename to providers/db/gen/sqlgen/db.go index 13a2738..8eab959 100644 --- a/providers/db/gen/sql/db.go +++ b/providers/db/gen/sqlgen/db.go @@ -1,8 +1,8 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.28.0 -package sql +package sqlgen import ( "context" diff --git a/providers/db/gen/sql/models.go b/providers/db/gen/sqlgen/models.go similarity index 69% rename from providers/db/gen/sql/models.go rename to providers/db/gen/sqlgen/models.go index b7d7996..35a61cd 100644 --- a/providers/db/gen/sql/models.go +++ b/providers/db/gen/sqlgen/models.go @@ -1,8 +1,8 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.28.0 -package sql +package sqlgen type Post struct { ID int64 @@ -16,12 +16,12 @@ type Post struct { } type PublishTarget struct { - ID int64 - SiteID int64 - PublishTargetType int64 - BaseUrl string - TargetSiteID string - TargetPublishKey string + ID int64 + SiteID int64 + TargetType int64 + BaseUrl string + TargetRef string + TargetKey string } type Site struct { diff --git a/providers/db/gen/sql/posts.sql.go b/providers/db/gen/sqlgen/posts.sql.go similarity index 97% rename from providers/db/gen/sql/posts.sql.go rename to providers/db/gen/sqlgen/posts.sql.go index 8cf5905..5ebafbf 100644 --- a/providers/db/gen/sql/posts.sql.go +++ b/providers/db/gen/sqlgen/posts.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.28.0 // source: posts.sql -package sql +package sqlgen import ( "context" diff --git a/providers/db/gen/sql/pubtargets.sql.go b/providers/db/gen/sqlgen/pubtargets.sql.go similarity index 71% rename from providers/db/gen/sql/pubtargets.sql.go rename to providers/db/gen/sqlgen/pubtargets.sql.go index a8898a1..fd2c499 100644 --- a/providers/db/gen/sql/pubtargets.sql.go +++ b/providers/db/gen/sqlgen/pubtargets.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.28.0 // source: pubtargets.sql -package sql +package sqlgen import ( "context" @@ -12,29 +12,29 @@ import ( const insertPublishTarget = `-- name: InsertPublishTarget :one INSERT INTO publish_targets ( site_id, - publish_target_type, + target_type, base_url, - target_site_id, - target_publish_key + target_ref, + target_key ) VALUES (?, ?, ?, ?, ?) RETURNING id ` type InsertPublishTargetParams struct { - SiteID int64 - PublishTargetType int64 - BaseUrl string - TargetSiteID string - TargetPublishKey string + SiteID int64 + TargetType int64 + BaseUrl string + TargetRef string + TargetKey string } func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) { row := q.db.QueryRowContext(ctx, insertPublishTarget, arg.SiteID, - arg.PublishTargetType, + arg.TargetType, arg.BaseUrl, - arg.TargetSiteID, - arg.TargetPublishKey, + arg.TargetRef, + arg.TargetKey, ) var id int64 err := row.Scan(&id) @@ -42,7 +42,7 @@ func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTarg } 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 = ? +SELECT id, site_id, target_type, base_url, target_ref, target_key FROM publish_targets WHERE site_id = ? ` func (q *Queries) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) ([]PublishTarget, error) { @@ -57,10 +57,10 @@ func (q *Queries) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) if err := rows.Scan( &i.ID, &i.SiteID, - &i.PublishTargetType, + &i.TargetType, &i.BaseUrl, - &i.TargetSiteID, - &i.TargetPublishKey, + &i.TargetRef, + &i.TargetKey, ); err != nil { return nil, err } diff --git a/providers/db/gen/sql/sites.sql.go b/providers/db/gen/sqlgen/sites.sql.go similarity index 76% rename from providers/db/gen/sql/sites.sql.go rename to providers/db/gen/sqlgen/sites.sql.go index 799e8fc..ea5d747 100644 --- a/providers/db/gen/sql/sites.sql.go +++ b/providers/db/gen/sqlgen/sites.sql.go @@ -1,9 +1,9 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.28.0 // source: sites.sql -package sql +package sqlgen import ( "context" @@ -31,6 +31,22 @@ func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, return id, err } +const selectSiteByID = `-- name: SelectSiteByID :one +SELECT id, owner_id, title, tagline FROM sites WHERE id = ? +` + +func (q *Queries) SelectSiteByID(ctx context.Context, id int64) (Site, error) { + row := q.db.QueryRowContext(ctx, selectSiteByID, id) + var i Site + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.Title, + &i.Tagline, + ) + return i, err +} + const selectSitesOwnedByUser = `-- name: SelectSitesOwnedByUser :many SELECT id, owner_id, title, tagline FROM sites WHERE owner_id = ? ` diff --git a/providers/db/gen/sqlgen/users.sql.go b/providers/db/gen/sqlgen/users.sql.go new file mode 100644 index 0000000..6bf9c52 --- /dev/null +++ b/providers/db/gen/sqlgen/users.sql.go @@ -0,0 +1,52 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: users.sql + +package sqlgen + +import ( + "context" +) + +const insertUser = `-- name: InsertUser :one +INSERT INTO users (username, password) VALUES (?, ?) RETURNING id +` + +type InsertUserParams struct { + Username string + Password string +} + +func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertUser, 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 +} + +const updateUser = `-- name: UpdateUser :exec +UPDATE users SET username = ?, password = ? WHERE id = ? +` + +type UpdateUserParams struct { + Username string + Password string + ID int64 +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { + _, err := q.db.ExecContext(ctx, updateUser, arg.Username, arg.Password, arg.ID) + return err +} diff --git a/providers/db/posts.go b/providers/db/posts.go new file mode 100644 index 0000000..13aae3d --- /dev/null +++ b/providers/db/posts.go @@ -0,0 +1,53 @@ +package db + +import ( + "context" + "time" + + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" +) + +func (db *Provider) SelectPostsOfSite(ctx context.Context, siteID int64) ([]*models.Post, error) { + rows, err := db.queries.SelectPostsOfSite(ctx, siteID) + if err != nil { + return nil, err + } + + posts := make([]*models.Post, len(rows)) + for i, row := range rows { + posts[i] = &models.Post{ + ID: row.ID, + SiteID: row.SiteID, + GUID: row.Guid, + Title: row.Title, + Body: row.Body, + Slug: row.Slug, + CreatedAt: time.Unix(row.CreatedAt, 0).UTC(), + PublishedAt: time.Unix(row.PublishedAt, 0).UTC(), + } + } + return posts, nil +} + +func (db *Provider) SavePost(ctx context.Context, post *models.Post) error { + if post.ID == 0 { + newID, err := db.queries.InsertPost(ctx, sqlgen.InsertPostParams{ + SiteID: post.SiteID, + Guid: post.GUID, + Title: post.Title, + Body: post.Body, + Slug: post.Slug, + CreatedAt: post.CreatedAt.Unix(), + PublishedAt: post.PublishedAt.Unix(), + }) + if err != nil { + return err + } + post.ID = newID + return nil + } + + // No update query defined in sqlgen yet + return nil +} diff --git a/providers/db/provider.go b/providers/db/provider.go index d33f0ed..b061b32 100644 --- a/providers/db/provider.go +++ b/providers/db/provider.go @@ -5,14 +5,14 @@ import ( "database/sql" "github.com/Southclaws/fault" - "github.com/lmika/blogging-tools/providers/db/sqlc/maindbq" - "github.com/lmika/blogging-tools/sql/maindb/schema" + "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" + "lmika.dev/lmika/weiro/sql/schema" migration "lmika.dev/pkg/litemigrate" ) type Provider struct { drvr *sql.DB - queries *maindbq.Queries + queries *sqlgen.Queries } func New(dbFile string) (*Provider, error) { @@ -31,7 +31,7 @@ func New(dbFile string) (*Provider, error) { return &Provider{ drvr: drvr, - queries: maindbq.New(drvr), + queries: sqlgen.New(drvr), }, nil } diff --git a/providers/db/provider_test.go b/providers/db/provider_test.go new file mode 100644 index 0000000..aed3e0b --- /dev/null +++ b/providers/db/provider_test.go @@ -0,0 +1,306 @@ +package db_test + +import ( + "context" + "encoding/base64" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db" + + _ "modernc.org/sqlite" +) + +func newTestDB(t *testing.T) *db.Provider { + t.Helper() + dbFile := filepath.Join(t.TempDir(), "test.db") + p, err := db.New(dbFile) + require.NoError(t, err) + t.Cleanup(func() { p.Close() }) + return p +} + +func TestProvider_Users(t *testing.T) { + ctx := context.Background() + p := newTestDB(t) + + t.Run("save and select user", func(t *testing.T) { + user := &models.User{ + Username: "alice", + PasswordHashed: []byte("hashed-password"), + } + + err := p.SaveUser(ctx, user) + require.NoError(t, err) + assert.NotZero(t, user.ID) + + got, err := p.SelectUserByUsername(ctx, "alice") + require.NoError(t, err) + assert.Equal(t, user.ID, got.ID) + assert.Equal(t, "alice", got.Username) + assert.Equal(t, []byte("hashed-password"), got.PasswordHashed) + }) + + t.Run("update user", func(t *testing.T) { + user := &models.User{ + Username: "bob", + PasswordHashed: []byte("old-password"), + } + err := p.SaveUser(ctx, user) + require.NoError(t, err) + + user.Username = "bob" + user.PasswordHashed = []byte("new-password") + err = p.SaveUser(ctx, user) + require.NoError(t, err) + + got, err := p.SelectUserByUsername(ctx, "bob") + require.NoError(t, err) + assert.Equal(t, []byte("new-password"), got.PasswordHashed) + }) +} + +func TestProvider_Sites(t *testing.T) { + ctx := context.Background() + p := newTestDB(t) + + // Create a user first (sites need an owner) + user := &models.User{ + Username: "testuser", + PasswordHashed: []byte("password"), + } + require.NoError(t, p.SaveUser(ctx, user)) + + t.Run("save and select sites", func(t *testing.T) { + site := &models.Site{ + OwnerID: user.ID, + Title: "My Blog", + Tagline: "A test blog", + } + + err := p.SaveSite(ctx, site) + require.NoError(t, err) + assert.NotZero(t, site.ID) + + sites, err := p.SelectSitesOwnedByUser(ctx, user.ID) + require.NoError(t, err) + require.Len(t, sites, 1) + assert.Equal(t, site.ID, sites[0].ID) + assert.Equal(t, user.ID, sites[0].OwnerID) + assert.Equal(t, "My Blog", sites[0].Title) + assert.Equal(t, "A test blog", sites[0].Tagline) + }) + + t.Run("select site by id", func(t *testing.T) { + site := &models.Site{ + OwnerID: user.ID, + Title: "Lookup Blog", + Tagline: "Find me by ID", + } + require.NoError(t, p.SaveSite(ctx, site)) + + got, err := p.SelectSiteByID(ctx, site.ID) + require.NoError(t, err) + assert.Equal(t, site.ID, got.ID) + assert.Equal(t, user.ID, got.OwnerID) + assert.Equal(t, "Lookup Blog", got.Title) + assert.Equal(t, "Find me by ID", got.Tagline) + }) + + t.Run("select sites for user with no sites", func(t *testing.T) { + otherUser := &models.User{ + Username: "otheruser", + PasswordHashed: []byte("password"), + } + require.NoError(t, p.SaveUser(ctx, otherUser)) + + sites, err := p.SelectSitesOwnedByUser(ctx, otherUser.ID) + require.NoError(t, err) + assert.Empty(t, sites) + }) +} + +func TestProvider_Posts(t *testing.T) { + ctx := context.Background() + p := newTestDB(t) + + // Create user and site + user := &models.User{ + Username: "testuser", + PasswordHashed: []byte("password"), + } + require.NoError(t, p.SaveUser(ctx, user)) + + site := &models.Site{ + OwnerID: user.ID, + Title: "My Blog", + Tagline: "A test blog", + } + require.NoError(t, p.SaveSite(ctx, site)) + + t.Run("save and select posts", func(t *testing.T) { + now := time.Date(2026, 2, 19, 12, 0, 0, 0, time.UTC) + post := &models.Post{ + SiteID: site.ID, + GUID: "post-001", + Title: "First Post", + Body: "Hello world", + Slug: "/2026/02/19/first-post", + CreatedAt: now, + PublishedAt: now, + } + + err := p.SavePost(ctx, post) + require.NoError(t, err) + assert.NotZero(t, post.ID) + + posts, err := p.SelectPostsOfSite(ctx, site.ID) + require.NoError(t, err) + require.Len(t, posts, 1) + assert.Equal(t, post.ID, posts[0].ID) + assert.Equal(t, site.ID, posts[0].SiteID) + assert.Equal(t, "post-001", posts[0].GUID) + assert.Equal(t, "First Post", posts[0].Title) + assert.Equal(t, "Hello world", posts[0].Body) + assert.Equal(t, "/2026/02/19/first-post", posts[0].Slug) + assert.Equal(t, now, posts[0].CreatedAt) + assert.Equal(t, now, posts[0].PublishedAt) + }) + + t.Run("posts ordered by created_at desc", func(t *testing.T) { + // Create a second site to isolate this test + site2 := &models.Site{ + OwnerID: user.ID, + Title: "Second Blog", + Tagline: "", + } + require.NoError(t, p.SaveSite(ctx, site2)) + + earlier := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + later := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) + + post1 := &models.Post{ + SiteID: site2.ID, + GUID: "old-post", + Title: "Old Post", + Body: "old", + Slug: "/old", + CreatedAt: earlier, + PublishedAt: earlier, + } + post2 := &models.Post{ + SiteID: site2.ID, + GUID: "new-post", + Title: "New Post", + Body: "new", + Slug: "/new", + CreatedAt: later, + PublishedAt: later, + } + + require.NoError(t, p.SavePost(ctx, post1)) + require.NoError(t, p.SavePost(ctx, post2)) + + posts, err := p.SelectPostsOfSite(ctx, site2.ID) + require.NoError(t, err) + require.Len(t, posts, 2) + assert.Equal(t, "New Post", posts[0].Title) + assert.Equal(t, "Old Post", posts[1].Title) + }) + + t.Run("select posts for site with no posts", func(t *testing.T) { + emptySite := &models.Site{ + OwnerID: user.ID, + Title: "Empty Blog", + Tagline: "", + } + require.NoError(t, p.SaveSite(ctx, emptySite)) + + posts, err := p.SelectPostsOfSite(ctx, emptySite.ID) + require.NoError(t, err) + assert.Empty(t, posts) + }) +} + +func TestProvider_PublishTargets(t *testing.T) { + ctx := context.Background() + p := newTestDB(t) + + // Create user and site + user := &models.User{ + Username: "testuser", + PasswordHashed: []byte("password"), + } + require.NoError(t, p.SaveUser(ctx, user)) + + site := &models.Site{ + OwnerID: user.ID, + Title: "My Blog", + Tagline: "A test blog", + } + require.NoError(t, p.SaveSite(ctx, site)) + + t.Run("save and select publish targets", func(t *testing.T) { + target := &models.SitePublishTarget{ + SiteID: site.ID, + TargetType: models.PublishTargetTypeNetlify, + BaseURL: "https://example.netlify.app", + TargetRef: "netlify-site-123", + TargetKey: "secret-key", + } + + err := p.SavePublishTarget(ctx, target) + require.NoError(t, err) + assert.NotZero(t, target.ID) + + targets, err := p.SelectPublishTargetsOfSite(ctx, site.ID) + require.NoError(t, err) + require.Len(t, targets, 1) + assert.Equal(t, target.ID, targets[0].ID) + assert.Equal(t, site.ID, targets[0].SiteID) + assert.Equal(t, models.PublishTargetTypeNetlify, targets[0].TargetType) + assert.Equal(t, "https://example.netlify.app", targets[0].BaseURL) + assert.Equal(t, "netlify-site-123", targets[0].TargetRef) + assert.Equal(t, "secret-key", targets[0].TargetKey) + }) + + t.Run("select targets for site with no targets", func(t *testing.T) { + emptySite := &models.Site{ + OwnerID: user.ID, + Title: "No Targets", + Tagline: "", + } + require.NoError(t, p.SaveSite(ctx, emptySite)) + + targets, err := p.SelectPublishTargetsOfSite(ctx, emptySite.ID) + require.NoError(t, err) + assert.Empty(t, targets) + }) +} + +// Verify that password encoding roundtrips correctly through base64 +func TestProvider_UserPasswordEncoding(t *testing.T) { + ctx := context.Background() + p := newTestDB(t) + + // Use bytes that aren't valid UTF-8 to verify binary safety + rawPassword := []byte{0x00, 0xff, 0x80, 0x7f, 0x01} + + user := &models.User{ + Username: "binuser", + PasswordHashed: rawPassword, + } + require.NoError(t, p.SaveUser(ctx, user)) + + got, err := p.SelectUserByUsername(ctx, "binuser") + require.NoError(t, err) + assert.Equal(t, rawPassword, got.PasswordHashed) + + // Verify it's stored as base64 (not raw bytes) - this is implicit + // from the implementation but good to confirm the roundtrip works + _ = base64.StdEncoding.EncodeToString(rawPassword) +} diff --git a/providers/db/pubtargets.go b/providers/db/pubtargets.go new file mode 100644 index 0000000..bbb6439 --- /dev/null +++ b/providers/db/pubtargets.go @@ -0,0 +1,48 @@ +package db + +import ( + "context" + + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" +) + +func (db *Provider) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) ([]models.SitePublishTarget, error) { + rows, err := db.queries.SelectPublishTargetsOfSite(ctx, siteID) + if err != nil { + return nil, err + } + + targets := make([]models.SitePublishTarget, len(rows)) + for i, row := range rows { + targets[i] = models.SitePublishTarget{ + ID: row.ID, + SiteID: row.SiteID, + TargetType: models.PublishTargetType(row.TargetType), + BaseURL: row.BaseUrl, + TargetRef: row.TargetRef, + TargetKey: row.TargetKey, + } + } + return targets, nil +} + +func (db *Provider) SavePublishTarget(ctx context.Context, target *models.SitePublishTarget) error { + if target.ID == 0 { + newID, err := db.queries.InsertPublishTarget(ctx, sqlgen.InsertPublishTargetParams{ + SiteID: target.SiteID, + TargetType: int64(target.TargetType), + BaseUrl: target.BaseURL, + TargetRef: target.TargetRef, + TargetKey: target.TargetKey, + }) + if err != nil { + return err + } + target.ID = newID + return nil + } + + // No update query defined in sqlgen yet + return nil +} diff --git a/providers/db/sites.go b/providers/db/sites.go new file mode 100644 index 0000000..eaf61fb --- /dev/null +++ b/providers/db/sites.go @@ -0,0 +1,58 @@ +package db + +import ( + "context" + + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" +) + +func (db *Provider) SelectSiteByID(ctx context.Context, id int64) (models.Site, error) { + row, err := db.queries.SelectSiteByID(ctx, id) + if err != nil { + return models.Site{}, err + } + + return models.Site{ + ID: row.ID, + OwnerID: row.OwnerID, + Title: row.Title, + Tagline: row.Tagline, + }, nil +} + +func (db *Provider) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]models.Site, error) { + rows, err := db.queries.SelectSitesOwnedByUser(ctx, ownerID) + if err != nil { + return nil, err + } + + sites := make([]models.Site, len(rows)) + for i, row := range rows { + sites[i] = models.Site{ + ID: row.ID, + OwnerID: row.OwnerID, + Title: row.Title, + Tagline: row.Tagline, + } + } + return sites, nil +} + +func (db *Provider) SaveSite(ctx context.Context, site *models.Site) error { + if site.ID == 0 { + newID, err := db.queries.InsertSite(ctx, sqlgen.InsertSiteParams{ + OwnerID: site.OwnerID, + Title: site.Title, + Tagline: site.Tagline, + }) + if err != nil { + return err + } + site.ID = newID + return nil + } + + // No update query defined in sqlgen yet + return nil +} diff --git a/providers/db/users.go b/providers/db/users.go new file mode 100644 index 0000000..73b3590 --- /dev/null +++ b/providers/db/users.go @@ -0,0 +1,49 @@ +package db + +import ( + "context" + "encoding/base64" + + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" +) + +func (db *Provider) SelectUserByUsername(ctx context.Context, username string) (models.User, error) { + res, err := db.queries.SelectUserByUsername(ctx, username) + if err != nil { + return models.User{}, err + } + + pwdBytes, err := base64.StdEncoding.DecodeString(res.Password) + if err != nil { + return models.User{}, err + } + + return models.User{ + ID: res.ID, + Username: res.Username, + PasswordHashed: pwdBytes, + }, nil +} + +func (db *Provider) SaveUser(ctx context.Context, user *models.User) error { + hashedPassword := base64.StdEncoding.EncodeToString(user.PasswordHashed) + + if user.ID == 0 { + newID, err := db.queries.InsertUser(ctx, sqlgen.InsertUserParams{ + Username: user.Username, + Password: hashedPassword, + }) + if err != nil { + return err + } + user.ID = newID + return nil + } + + return db.queries.UpdateUser(ctx, sqlgen.UpdateUserParams{ + ID: user.ID, + Username: user.Username, + Password: hashedPassword, + }) +} diff --git a/providers/sitebuilder/builder.go b/providers/sitebuilder/builder.go index cba9693..b07ea79 100644 --- a/providers/sitebuilder/builder.go +++ b/providers/sitebuilder/builder.go @@ -109,7 +109,7 @@ func (b *Builder) renderPost(post *models.Post) (postSingleData, error) { return postSingleData{ commonData: commonData{Site: b.site}, Path: postPath, - Meta: post, + Post: post, HTML: template.HTML(md.String()), }, nil } diff --git a/providers/sitebuilder/builder_test.go b/providers/sitebuilder/builder_test.go index 249b5f8..2564f6d 100644 --- a/providers/sitebuilder/builder_test.go +++ b/providers/sitebuilder/builder_test.go @@ -16,7 +16,7 @@ func TestBuilder_BuildSite(t *testing.T) { t.Run("build site", func(t *testing.T) { tmpls := fstest.MapFS{ "posts_single.html": {Data: []byte(`{{ .HTML }}`)}, - "posts_list.html": {Data: []byte(`{{ range .Posts}}{{.Meta.Title}},{{ end }}`)}, + "posts_list.html": {Data: []byte(`{{ range .Posts}}{{.Post.Title}},{{ end }}`)}, "layout_main.html": {Data: []byte(`{{ .Body }}`)}, } @@ -59,4 +59,4 @@ func TestBuilder_BuildSite(t *testing.T) { } }) -} \ No newline at end of file +} diff --git a/providers/sitebuilder/tmpls.go b/providers/sitebuilder/tmpls.go index b9a098a..812a14d 100644 --- a/providers/sitebuilder/tmpls.go +++ b/providers/sitebuilder/tmpls.go @@ -38,7 +38,7 @@ type commonData struct { type postSingleData struct { commonData - Meta *models.Post + Post *models.Post HTML template.HTML Path string } diff --git a/providers/sitereader/models.go b/providers/sitereader/models.go index 276576d..c7114e0 100644 --- a/providers/sitereader/models.go +++ b/providers/sitereader/models.go @@ -7,9 +7,8 @@ import ( ) type ReadSiteModels struct { - Site models.Site - Target models.SitePublishTarget - Posts []*models.Post + Site models.Site + Posts []*models.Post } type siteMeta struct { diff --git a/providers/sitereader/provider.go b/providers/sitereader/provider.go index c911d04..1365d4b 100644 --- a/providers/sitereader/provider.go +++ b/providers/sitereader/provider.go @@ -39,14 +39,10 @@ func (p *Provider) ReadSite() (ReadSiteModels, error) { Title: meta.Title, Tagline: meta.Tagline, } - publishTarget := models.SitePublishTarget{ - BaseURL: meta.BaseURL, - } return ReadSiteModels{ - Site: site, - Target: publishTarget, - Posts: posts, + Site: site, + Posts: posts, }, nil } diff --git a/services/importer/service.go b/services/importer/service.go new file mode 100644 index 0000000..473d54a --- /dev/null +++ b/services/importer/service.go @@ -0,0 +1,51 @@ +package importer + +import ( + "context" + "os" + + "emperror.dev/errors" + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/lmika/weiro/providers/sitereader" +) + +type Service struct { + db *db.Provider +} + +func New(db *db.Provider) *Service { + return &Service{ + db: db, + } +} + +func (s *Service) Import(ctx context.Context, sitePath string) (models.Site, error) { + user, ok := models.GetUser(ctx) + if !ok { + return models.Site{}, models.UserRequiredError + } + + sr := sitereader.New(os.DirFS(sitePath)) + + readSite, err := sr.ReadSite() + if err != nil { + return models.Site{}, errors.Wrap(err, "failed to read site") + } + + site := readSite.Site + site.OwnerID = user.ID + + if err := s.db.SaveSite(ctx, &site); err != nil { + return models.Site{}, errors.Wrap(err, "failed to save site") + } + + for _, post := range readSite.Posts { + post.SiteID = site.ID + if err := s.db.SavePost(ctx, post); err != nil { + return models.Site{}, errors.Wrap(err, "failed to save post") + } + } + + return site, nil +} diff --git a/services/publisher/service.go b/services/publisher/service.go new file mode 100644 index 0000000..b9c50a1 --- /dev/null +++ b/services/publisher/service.go @@ -0,0 +1,80 @@ +package publisher + +import ( + "context" + "log" + + "lmika.dev/lmika/weiro/layouts/simplecss" + "lmika.dev/lmika/weiro/models" + "lmika.dev/lmika/weiro/models/pubmodel" + "lmika.dev/lmika/weiro/providers/db" + "lmika.dev/lmika/weiro/providers/sitebuilder" +) + +type Publisher struct { + db *db.Provider +} + +func New(db *db.Provider) *Publisher { + return &Publisher{ + db: db, + } +} + +func (p *Publisher) Publish(ctx context.Context, siteID int64) error { + // Fetch site, ensure user is owner + site, err := p.db.SelectSiteByID(ctx, siteID) + if err != nil { + return err + } + + user, ok := models.GetUser(ctx) + if !ok { + return models.UserRequiredError + } else if user.ID != site.OwnerID { + return models.PermissionError + } + + targets, err := p.db.SelectPublishTargetsOfSite(ctx, siteID) + if err != nil { + return err + } + + // Fetch all content of site + posts, err := p.db.SelectPostsOfSite(ctx, siteID) + if err != nil { + return err + } + + for _, target := range targets { + pubSite := pubmodel.Site{ + Site: site, + Posts: posts, + BaseURL: target.BaseURL, + } + + if err := p.publishSite(ctx, pubSite, target); err != nil { + return err + } + } + + return nil +} + +func (p *Publisher) publishSite(ctx context.Context, pubSite pubmodel.Site, target models.SitePublishTarget) error { + sb, err := sitebuilder.New(pubSite, sitebuilder.Options{ + BasePosts: "/posts", + TemplatesFS: simplecss.FS, + }) + if err != nil { + return err + } + + switch target.TargetType { + case models.PublishTargetTypeLocalFS: + log.Printf("Building site at %s", target.TargetRef) + return sb.BuildSite(target.TargetRef) + } + + return nil +} diff --git a/sql/queries/pubtargets.sql b/sql/queries/pubtargets.sql index d0f39c6..a175dda 100644 --- a/sql/queries/pubtargets.sql +++ b/sql/queries/pubtargets.sql @@ -4,9 +4,9 @@ SELECT * FROM publish_targets WHERE site_id = ?; -- name: InsertPublishTarget :one INSERT INTO publish_targets ( site_id, - publish_target_type, + target_type, base_url, - target_site_id, - target_publish_key + target_ref, + target_key ) VALUES (?, ?, ?, ?, ?) RETURNING id; \ No newline at end of file diff --git a/sql/queries/sites.sql b/sql/queries/sites.sql index e751b82..0ea7567 100644 --- a/sql/queries/sites.sql +++ b/sql/queries/sites.sql @@ -1,6 +1,9 @@ -- name: SelectSitesOwnedByUser :many SELECT * FROM sites WHERE owner_id = ?; +-- name: SelectSiteByID :one +SELECT * FROM sites WHERE id = ?; + -- name: InsertSite :one INSERT INTO sites ( owner_id, diff --git a/sql/queries/users.sql b/sql/queries/users.sql index 547b516..ec4c69d 100644 --- a/sql/queries/users.sql +++ b/sql/queries/users.sql @@ -1,5 +1,8 @@ -- 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 +-- name: InsertUser :one +INSERT INTO users (username, password) VALUES (?, ?) RETURNING id; + +-- name: UpdateUser :exec +UPDATE users SET username = ?, password = ? WHERE id = ?; \ No newline at end of file diff --git a/sql/schema/01_init.up.sql b/sql/schema/01_init.up.sql index 282c77d..f5a83b5 100644 --- a/sql/schema/01_init.up.sql +++ b/sql/schema/01_init.up.sql @@ -13,15 +13,15 @@ CREATE TABLE sites ( FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE ); -CREATE INDEX idx_site_owner ON site (owner_id); +CREATE INDEX idx_site_owner ON sites (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 + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id INTEGER NOT NULL, + target_type INTEGER NOT NULL, + base_url TEXT NOT NULL, + target_ref TEXT NOT NULL, + target_key TEXT NOT NULL ); CREATE INDEX idx_publish_targets_site ON publish_targets (site_id); diff --git a/sql/schema/fs.go b/sql/schema/fs.go new file mode 100644 index 0000000..ba7d7bd --- /dev/null +++ b/sql/schema/fs.go @@ -0,0 +1,6 @@ +package schema + +import "embed" + +//go:embed *.sql +var FS embed.FS diff --git a/sqlc.yaml b/sqlc.yaml index e0f2272..a3f8d56 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -5,5 +5,5 @@ sql: schema: "sql/schema" gen: go: - package: "sql" - out: "providers/db/gen/sql" \ No newline at end of file + package: "sqlgen" + out: "providers/db/gen/sqlgen" \ No newline at end of file