Added a database

This commit is contained in:
Leon Mika 2026-02-19 22:29:44 +11:00
parent ebaec3d296
commit 8136655336
35 changed files with 925 additions and 134 deletions

View 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
View file

@ -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
)

42
go.sum
View file

@ -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=

View file

@ -1,5 +1,5 @@
{{ range .Posts }}
{{ if .Meta.Title }}<h3>{{ .Meta.Title }}</h3>{{ end }}
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
<a href="{{ url_abs .Path }}">{{ format_date .Meta.Date }}</a>
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>
{{ end }}

View file

@ -1,3 +1,3 @@
{{ if .Meta.Title }}<h3>{{ .Meta.Title }}</h3>{{ end }}
{{ if .Post.Title }}<h3>{{ .Post.Title }}</h3>{{ end }}
{{ .HTML }}
<a href="{{ url_abs .Path }}">{{ format_date .Meta.Date }}</a>
<a href="{{ url_abs .Path }}">{{ format_date .Post.PublishedAt }}</a>

55
main.go
View file

@ -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)
}

16
models/ctx.go Normal file
View 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
View file

@ -0,0 +1,6 @@
package models
import "emperror.dev/errors"
var UserRequiredError = errors.New("user required")
var PermissionError = errors.New("permission denied")

View file

@ -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
}
/*

View file

@ -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
}

View file

@ -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"

View file

@ -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 {

View file

@ -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"

View file

@ -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
}

View file

@ -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 = ?
`

View 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
View 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
}

View file

@ -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
}

View 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)
}

View 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
View 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
View 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,
})
}

View file

@ -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
}

View file

@ -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}}<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 }}`)},
}

View file

@ -38,7 +38,7 @@ type commonData struct {
type postSingleData struct {
commonData
Meta *models.Post
Post *models.Post
HTML template.HTML
Path string
}

View file

@ -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 {

View file

@ -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
}

View 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
}

View 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
}

View file

@ -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;

View file

@ -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,

View file

@ -1,5 +1,8 @@
-- name: SelectUserByUsername :one
SELECT * FROM users WHERE username = ?;
-- name: InsertUserByUsername :one
-- name: InsertUser :one
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id;
-- name: UpdateUser :exec
UPDATE users SET username = ?, password = ? WHERE id = ?;

View file

@ -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);

6
sql/schema/fs.go Normal file
View file

@ -0,0 +1,6 @@
package schema
import "embed"
//go:embed *.sql
var FS embed.FS

View file

@ -5,5 +5,5 @@ sql:
schema: "sql/schema"
gen:
go:
package: "sql"
out: "providers/db/gen/sql"
package: "sqlgen"
out: "providers/db/gen/sqlgen"