Added a database
This commit is contained in:
parent
ebaec3d296
commit
8136655336
18
_test-site/posts/2026/02/19-about-a-db.md
Normal file
18
_test-site/posts/2026/02/19-about-a-db.md
Normal file
|
|
@ -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.
|
||||||
13
go.mod
13
go.mod
|
|
@ -3,9 +3,13 @@ module lmika.dev/lmika/weiro
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
emperror.dev/errors v0.8.1
|
||||||
github.com/Southclaws/fault 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
|
lmika.dev/pkg/litemigrate v0.1.0
|
||||||
|
modernc.org/sqlite v1.46.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -15,15 +19,14 @@ require (
|
||||||
github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect
|
github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
github.com/yuin/goldmark v1.7.16 // indirect
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/sys v0.37.0 // 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/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.46.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
42
go.sum
42
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 h1:mgqqdC6kUBQ6ExMALZ0nNaDfNJD5h2+wq3se5mAyX+8=
|
||||||
github.com/Southclaws/fault v0.8.1/go.mod h1:VUVkAWutC59SL16s6FTqf3I6I2z77RmnaW5XRz4bLOE=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:WHM70gRzPTKHSKm/wf2kp3HErXzMpmcqfkGjHlhtu1Y=
|
||||||
github.com/lmika/blogging-tools v0.0.0-20240630114557-8db2b3aa93e6/go.mod h1:w4rGqiE0+/FDUNWiIhfPGSPfT948/9Yw+cja12zOb4o=
|
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 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/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 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
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 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:DBEJahbQO7W3uEmAOQGg1URBWYimg0ClWHi83M2MZwk=
|
||||||
lmika.dev/pkg/litemigrate v0.1.0/go.mod h1:GQWWDiMZGQaVspcwKNq8vIBPN5H+KsUo/VBIeh9OfLg=
|
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 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
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 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
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=
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{{ range .Posts }}
|
{{ range .Posts }}
|
||||||
{{ if .Meta.Title }}<h3>{{ .Meta.Title }}</h3>{{ end }}
|
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
|
||||||
{{ .HTML }}
|
{{ .HTML }}
|
||||||
<a href="{{ url_abs .Path }}">{{ format_date .Meta.Date }}</a>
|
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{{ if .Meta.Title }}<h3>{{ .Meta.Title }}</h3>{{ end }}
|
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
|
||||||
{{ .HTML }}
|
{{ .HTML }}
|
||||||
<a href="{{ url_abs .Path }}">{{ format_date .Meta.Date }}</a>
|
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
|
||||||
55
main.go
55
main.go
|
|
@ -1,38 +1,57 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
|
|
||||||
"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"
|
"lmika.dev/lmika/weiro/services/importer"
|
||||||
"lmika.dev/lmika/weiro/providers/sitereader"
|
"lmika.dev/lmika/weiro/services/publisher"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
site := pubmodel.Site{
|
target := models.SitePublishTarget{
|
||||||
Site: readSite.Site,
|
SiteID: site.ID,
|
||||||
BaseURL: readSite.Target.BaseURL,
|
BaseURL: "https://jolly-boba-9e2486.netlify.app",
|
||||||
Posts: readSite.Posts,
|
TargetType: models.PublishTargetTypeLocalFS,
|
||||||
|
TargetRef: "build/out",
|
||||||
|
TargetKey: "",
|
||||||
}
|
}
|
||||||
|
if err := dbp.SavePublishTarget(ctx, &target); err != nil {
|
||||||
sb, err := sitebuilder.New(site, sitebuilder.Options{
|
|
||||||
BasePosts: "/posts",
|
|
||||||
TemplatesFS: simplecss.FS,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sb.BuildSite("build/out"); err != nil {
|
if err := publisherSvc.Publish(ctx, site.ID); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
models/ctx.go
Normal file
16
models/ctx.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
6
models/errors.go
Normal file
6
models/errors.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "emperror.dev/errors"
|
||||||
|
|
||||||
|
var UserRequiredError = errors.New("user required")
|
||||||
|
var PermissionError = errors.New("permission denied")
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
type PublishTargetType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PublishTargetTypeNone int = iota
|
PublishTargetTypeNone PublishTargetType = 0
|
||||||
PublishTargetTypeNetlify
|
PublishTargetTypeLocalFS PublishTargetType = 1
|
||||||
|
PublishTargetTypeNetlify PublishTargetType = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
|
|
@ -15,12 +18,13 @@ type Site struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SitePublishTarget struct {
|
type SitePublishTarget struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
PublishTargetType int
|
|
||||||
BaseURL string
|
BaseURL string
|
||||||
TargetSiteID string
|
TargetType PublishTargetType
|
||||||
TargetPublishKey string
|
TargetRef string
|
||||||
|
TargetKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.28.0
|
||||||
|
|
||||||
package sql
|
package sqlgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.28.0
|
||||||
|
|
||||||
package sql
|
package sqlgen
|
||||||
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
|
@ -16,12 +16,12 @@ type Post struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublishTarget struct {
|
type PublishTarget struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
PublishTargetType int64
|
TargetType int64
|
||||||
BaseUrl string
|
BaseUrl string
|
||||||
TargetSiteID string
|
TargetRef string
|
||||||
TargetPublishKey string
|
TargetKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.28.0
|
||||||
// source: posts.sql
|
// source: posts.sql
|
||||||
|
|
||||||
package sql
|
package sqlgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.28.0
|
||||||
// source: pubtargets.sql
|
// source: pubtargets.sql
|
||||||
|
|
||||||
package sql
|
package sqlgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -12,29 +12,29 @@ import (
|
||||||
const insertPublishTarget = `-- name: InsertPublishTarget :one
|
const insertPublishTarget = `-- name: InsertPublishTarget :one
|
||||||
INSERT INTO publish_targets (
|
INSERT INTO publish_targets (
|
||||||
site_id,
|
site_id,
|
||||||
publish_target_type,
|
target_type,
|
||||||
base_url,
|
base_url,
|
||||||
target_site_id,
|
target_ref,
|
||||||
target_publish_key
|
target_key
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertPublishTargetParams struct {
|
type InsertPublishTargetParams struct {
|
||||||
SiteID int64
|
SiteID int64
|
||||||
PublishTargetType int64
|
TargetType int64
|
||||||
BaseUrl string
|
BaseUrl string
|
||||||
TargetSiteID string
|
TargetRef string
|
||||||
TargetPublishKey string
|
TargetKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) {
|
func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) {
|
||||||
row := q.db.QueryRowContext(ctx, insertPublishTarget,
|
row := q.db.QueryRowContext(ctx, insertPublishTarget,
|
||||||
arg.SiteID,
|
arg.SiteID,
|
||||||
arg.PublishTargetType,
|
arg.TargetType,
|
||||||
arg.BaseUrl,
|
arg.BaseUrl,
|
||||||
arg.TargetSiteID,
|
arg.TargetRef,
|
||||||
arg.TargetPublishKey,
|
arg.TargetKey,
|
||||||
)
|
)
|
||||||
var id int64
|
var id int64
|
||||||
err := row.Scan(&id)
|
err := row.Scan(&id)
|
||||||
|
|
@ -42,7 +42,7 @@ func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTarg
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectPublishTargetsOfSite = `-- name: SelectPublishTargetsOfSite :many
|
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) {
|
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(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.SiteID,
|
&i.SiteID,
|
||||||
&i.PublishTargetType,
|
&i.TargetType,
|
||||||
&i.BaseUrl,
|
&i.BaseUrl,
|
||||||
&i.TargetSiteID,
|
&i.TargetRef,
|
||||||
&i.TargetPublishKey,
|
&i.TargetKey,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.28.0
|
||||||
// source: sites.sql
|
// source: sites.sql
|
||||||
|
|
||||||
package sql
|
package sqlgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -31,6 +31,22 @@ func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64,
|
||||||
return id, err
|
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
|
const selectSitesOwnedByUser = `-- name: SelectSitesOwnedByUser :many
|
||||||
SELECT id, owner_id, title, tagline FROM sites WHERE owner_id = ?
|
SELECT id, owner_id, title, tagline FROM sites WHERE owner_id = ?
|
||||||
`
|
`
|
||||||
52
providers/db/gen/sqlgen/users.sql.go
Normal file
52
providers/db/gen/sqlgen/users.sql.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
53
providers/db/posts.go
Normal file
53
providers/db/posts.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -5,14 +5,14 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/Southclaws/fault"
|
"github.com/Southclaws/fault"
|
||||||
"github.com/lmika/blogging-tools/providers/db/sqlc/maindbq"
|
"lmika.dev/lmika/weiro/providers/db/gen/sqlgen"
|
||||||
"github.com/lmika/blogging-tools/sql/maindb/schema"
|
"lmika.dev/lmika/weiro/sql/schema"
|
||||||
migration "lmika.dev/pkg/litemigrate"
|
migration "lmika.dev/pkg/litemigrate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
drvr *sql.DB
|
drvr *sql.DB
|
||||||
queries *maindbq.Queries
|
queries *sqlgen.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dbFile string) (*Provider, error) {
|
func New(dbFile string) (*Provider, error) {
|
||||||
|
|
@ -31,7 +31,7 @@ func New(dbFile string) (*Provider, error) {
|
||||||
|
|
||||||
return &Provider{
|
return &Provider{
|
||||||
drvr: drvr,
|
drvr: drvr,
|
||||||
queries: maindbq.New(drvr),
|
queries: sqlgen.New(drvr),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
306
providers/db/provider_test.go
Normal file
306
providers/db/provider_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
48
providers/db/pubtargets.go
Normal file
48
providers/db/pubtargets.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
58
providers/db/sites.go
Normal file
58
providers/db/sites.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
49
providers/db/users.go
Normal file
49
providers/db/users.go
Normal file
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -109,7 +109,7 @@ func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
|
||||||
return postSingleData{
|
return postSingleData{
|
||||||
commonData: commonData{Site: b.site},
|
commonData: commonData{Site: b.site},
|
||||||
Path: postPath,
|
Path: postPath,
|
||||||
Meta: post,
|
Post: post,
|
||||||
HTML: template.HTML(md.String()),
|
HTML: template.HTML(md.String()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func TestBuilder_BuildSite(t *testing.T) {
|
||||||
t.Run("build site", func(t *testing.T) {
|
t.Run("build site", func(t *testing.T) {
|
||||||
tmpls := fstest.MapFS{
|
tmpls := fstest.MapFS{
|
||||||
"posts_single.html": {Data: []byte(`{{ .HTML }}`)},
|
"posts_single.html": {Data: []byte(`{{ .HTML }}`)},
|
||||||
"posts_list.html": {Data: []byte(`{{ range .Posts}}<a href="{{url_abs .Path}}">{{.Meta.Title}}</a>,{{ end }}`)},
|
"posts_list.html": {Data: []byte(`{{ range .Posts}}<a href="{{url_abs .Path}}">{{.Post.Title}}</a>,{{ end }}`)},
|
||||||
"layout_main.html": {Data: []byte(`{{ .Body }}`)},
|
"layout_main.html": {Data: []byte(`{{ .Body }}`)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ type commonData struct {
|
||||||
|
|
||||||
type postSingleData struct {
|
type postSingleData struct {
|
||||||
commonData
|
commonData
|
||||||
Meta *models.Post
|
Post *models.Post
|
||||||
HTML template.HTML
|
HTML template.HTML
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReadSiteModels struct {
|
type ReadSiteModels struct {
|
||||||
Site models.Site
|
Site models.Site
|
||||||
Target models.SitePublishTarget
|
Posts []*models.Post
|
||||||
Posts []*models.Post
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type siteMeta struct {
|
type siteMeta struct {
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,10 @@ func (p *Provider) ReadSite() (ReadSiteModels, error) {
|
||||||
Title: meta.Title,
|
Title: meta.Title,
|
||||||
Tagline: meta.Tagline,
|
Tagline: meta.Tagline,
|
||||||
}
|
}
|
||||||
publishTarget := models.SitePublishTarget{
|
|
||||||
BaseURL: meta.BaseURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ReadSiteModels{
|
return ReadSiteModels{
|
||||||
Site: site,
|
Site: site,
|
||||||
Target: publishTarget,
|
Posts: posts,
|
||||||
Posts: posts,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
51
services/importer/service.go
Normal file
51
services/importer/service.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
80
services/publisher/service.go
Normal file
80
services/publisher/service.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,9 @@ SELECT * FROM publish_targets WHERE site_id = ?;
|
||||||
-- name: InsertPublishTarget :one
|
-- name: InsertPublishTarget :one
|
||||||
INSERT INTO publish_targets (
|
INSERT INTO publish_targets (
|
||||||
site_id,
|
site_id,
|
||||||
publish_target_type,
|
target_type,
|
||||||
base_url,
|
base_url,
|
||||||
target_site_id,
|
target_ref,
|
||||||
target_publish_key
|
target_key
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?)
|
||||||
RETURNING id;
|
RETURNING id;
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
-- name: SelectSitesOwnedByUser :many
|
-- name: SelectSitesOwnedByUser :many
|
||||||
SELECT * FROM sites WHERE owner_id = ?;
|
SELECT * FROM sites WHERE owner_id = ?;
|
||||||
|
|
||||||
|
-- name: SelectSiteByID :one
|
||||||
|
SELECT * FROM sites WHERE id = ?;
|
||||||
|
|
||||||
-- name: InsertSite :one
|
-- name: InsertSite :one
|
||||||
INSERT INTO sites (
|
INSERT INTO sites (
|
||||||
owner_id,
|
owner_id,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
-- name: SelectUserByUsername :one
|
-- name: SelectUserByUsername :one
|
||||||
SELECT * FROM users WHERE username = ?;
|
SELECT * FROM users WHERE username = ?;
|
||||||
|
|
||||||
-- name: InsertUserByUsername :one
|
-- name: InsertUser :one
|
||||||
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id;
|
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id;
|
||||||
|
|
||||||
|
-- name: UpdateUser :exec
|
||||||
|
UPDATE users SET username = ?, password = ? WHERE id = ?;
|
||||||
|
|
@ -13,15 +13,15 @@ CREATE TABLE sites (
|
||||||
|
|
||||||
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE
|
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 (
|
CREATE TABLE publish_targets (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
site_id INTEGER NOT NULL,
|
site_id INTEGER NOT NULL,
|
||||||
publish_target_type INTEGER NOT NULL,
|
target_type INTEGER NOT NULL,
|
||||||
base_url TEXT NOT NULL,
|
base_url TEXT NOT NULL,
|
||||||
target_site_id TEXT NOT NULL,
|
target_ref TEXT NOT NULL,
|
||||||
target_publish_key TEXT NOT NULL
|
target_key TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_publish_targets_site ON publish_targets (site_id);
|
CREATE INDEX idx_publish_targets_site ON publish_targets (site_id);
|
||||||
|
|
||||||
|
|
|
||||||
6
sql/schema/fs.go
Normal file
6
sql/schema/fs.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var FS embed.FS
|
||||||
Loading…
Reference in a new issue