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