Modified models to support a DB

This commit is contained in:
Leon Mika 2026-02-19 21:21:27 +11:00
parent 3591e0c723
commit ebaec3d296
33 changed files with 675 additions and 64 deletions

View file

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package sql
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View file

@ -0,0 +1,38 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package sql
type Post struct {
ID int64
SiteID int64
Guid string
Title string
Body string
Slug string
CreatedAt int64
PublishedAt int64
}
type PublishTarget struct {
ID int64
SiteID int64
PublishTargetType int64
BaseUrl string
TargetSiteID string
TargetPublishKey string
}
type Site struct {
ID int64
OwnerID int64
Title string
Tagline string
}
type User struct {
ID int64
Username string
Password string
}

View file

@ -0,0 +1,84 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: posts.sql
package sql
import (
"context"
)
const insertPost = `-- name: InsertPost :one
INSERT INTO posts (
site_id,
guid,
title,
body,
slug,
created_at,
published_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id
`
type InsertPostParams struct {
SiteID int64
Guid string
Title string
Body string
Slug string
CreatedAt int64
PublishedAt int64
}
func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertPost,
arg.SiteID,
arg.Guid,
arg.Title,
arg.Body,
arg.Slug,
arg.CreatedAt,
arg.PublishedAt,
)
var id int64
err := row.Scan(&id)
return id, err
}
const selectPostsOfSite = `-- name: SelectPostsOfSite :many
SELECT id, site_id, guid, title, body, slug, created_at, published_at FROM posts WHERE site_id = ? ORDER BY created_at DESC LIMIT 10
`
func (q *Queries) SelectPostsOfSite(ctx context.Context, siteID int64) ([]Post, error) {
rows, err := q.db.QueryContext(ctx, selectPostsOfSite, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Post
for rows.Next() {
var i Post
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Guid,
&i.Title,
&i.Body,
&i.Slug,
&i.CreatedAt,
&i.PublishedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View file

@ -0,0 +1,76 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: pubtargets.sql
package sql
import (
"context"
)
const insertPublishTarget = `-- name: InsertPublishTarget :one
INSERT INTO publish_targets (
site_id,
publish_target_type,
base_url,
target_site_id,
target_publish_key
) VALUES (?, ?, ?, ?, ?)
RETURNING id
`
type InsertPublishTargetParams struct {
SiteID int64
PublishTargetType int64
BaseUrl string
TargetSiteID string
TargetPublishKey string
}
func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertPublishTarget,
arg.SiteID,
arg.PublishTargetType,
arg.BaseUrl,
arg.TargetSiteID,
arg.TargetPublishKey,
)
var id int64
err := row.Scan(&id)
return id, err
}
const selectPublishTargetsOfSite = `-- name: SelectPublishTargetsOfSite :many
SELECT id, site_id, publish_target_type, base_url, target_site_id, target_publish_key FROM publish_targets WHERE site_id = ?
`
func (q *Queries) SelectPublishTargetsOfSite(ctx context.Context, siteID int64) ([]PublishTarget, error) {
rows, err := q.db.QueryContext(ctx, selectPublishTargetsOfSite, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []PublishTarget
for rows.Next() {
var i PublishTarget
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.PublishTargetType,
&i.BaseUrl,
&i.TargetSiteID,
&i.TargetPublishKey,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View file

@ -0,0 +1,64 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: sites.sql
package sql
import (
"context"
)
const insertSite = `-- name: InsertSite :one
INSERT INTO sites (
owner_id,
title,
tagline
) VALUES (?, ?, ?)
RETURNING id
`
type InsertSiteParams struct {
OwnerID int64
Title string
Tagline string
}
func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertSite, arg.OwnerID, arg.Title, arg.Tagline)
var id int64
err := row.Scan(&id)
return id, err
}
const selectSitesOwnedByUser = `-- name: SelectSitesOwnedByUser :many
SELECT id, owner_id, title, tagline FROM sites WHERE owner_id = ?
`
func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]Site, error) {
rows, err := q.db.QueryContext(ctx, selectSitesOwnedByUser, ownerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Site
for rows.Next() {
var i Site
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.Title,
&i.Tagline,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View file

@ -0,0 +1,37 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: users.sql
package sql
import (
"context"
)
const insertUserByUsername = `-- name: InsertUserByUsername :one
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id
`
type InsertUserByUsernameParams struct {
Username string
Password string
}
func (q *Queries) InsertUserByUsername(ctx context.Context, arg InsertUserByUsernameParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertUserByUsername, arg.Username, arg.Password)
var id int64
err := row.Scan(&id)
return id, err
}
const selectUserByUsername = `-- name: SelectUserByUsername :one
SELECT id, username, password FROM users WHERE username = ?
`
func (q *Queries) SelectUserByUsername(ctx context.Context, username string) (User, error) {
row := q.db.QueryRowContext(ctx, selectUserByUsername, username)
var i User
err := row.Scan(&i.ID, &i.Username, &i.Password)
return i, err
}

40
providers/db/provider.go Normal file
View file

@ -0,0 +1,40 @@
package db
import (
"context"
"database/sql"
"github.com/Southclaws/fault"
"github.com/lmika/blogging-tools/providers/db/sqlc/maindbq"
"github.com/lmika/blogging-tools/sql/maindb/schema"
migration "lmika.dev/pkg/litemigrate"
)
type Provider struct {
drvr *sql.DB
queries *maindbq.Queries
}
func New(dbFile string) (*Provider, error) {
drvr, err := sql.Open("sqlite", dbFile)
if err != nil {
return nil, fault.Wrap(err)
}
if err := migration.New(schema.FS, drvr).MigrateUp(context.Background()); err != nil {
return nil, fault.Wrap(err)
}
if _, err := drvr.Exec(`PRAGMA foreign_keys = 1;`); err != nil {
return nil, fault.Wrap(err)
}
return &Provider{
drvr: drvr,
queries: maindbq.New(drvr),
}, nil
}
func (db *Provider) Close() error {
return db.drvr.Close()
}

View file

@ -16,16 +16,17 @@ import (
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/models/pubmodel"
)
type Builder struct {
site models.Site
site pubmodel.Site
gmMarkdown goldmark.Markdown
opts Options
tmpls *template.Template
}
func New(site models.Site, opts Options) (*Builder, error) {
func New(site pubmodel.Site, opts Options) (*Builder, error) {
tmpls, err := template.New("").
Funcs(templateFns(site, opts)).
ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain)
@ -75,11 +76,11 @@ func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) erro
copy(postCopy, postList)
sort.Slice(postCopy, func(i, j int) bool {
return postCopy[i].Meta.Date.After(postCopy[j].Meta.Date)
return postCopy[i].PublishedAt.After(postCopy[j].PublishedAt)
})
pl := postListData{
commonData: commonData{Site: b.site.Meta},
commonData: commonData{Site: b.site},
}
for _, post := range postCopy {
rp, err := b.renderPost(post)
@ -95,20 +96,20 @@ func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) erro
}
func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
postPath := post.Meta.Slug
postPath := post.Slug
if b.opts.BasePosts != "" {
postPath = filepath.Join(b.opts.BasePosts, strings.TrimPrefix(postPath, "/"))
}
var md bytes.Buffer
if err := b.gmMarkdown.Convert([]byte(post.Content), &md); err != nil {
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Meta.Slug, err)
if err := b.gmMarkdown.Convert([]byte(post.Body), &md); err != nil {
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err)
}
return postSingleData{
commonData: commonData{Site: b.site.Meta},
commonData: commonData{Site: b.site},
Path: postPath,
Meta: post.Meta,
Meta: post,
HTML: template.HTML(md.String()),
}, nil
}
@ -152,7 +153,7 @@ func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) err
}
return b.tmpls.ExecuteTemplate(w, tmplNameLayoutMain, layoutData{
commonData: commonData{Site: b.site.Meta},
commonData: commonData{Site: b.site},
Body: template.HTML(buf.String()),
})
}

View file

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/models/pubmodel"
"lmika.dev/lmika/weiro/providers/sitebuilder"
)
@ -19,24 +20,18 @@ func TestBuilder_BuildSite(t *testing.T) {
"layout_main.html": {Data: []byte(`{{ .Body }}`)},
}
site := models.Site{
Meta: models.SiteMeta{
BaseURL: "https://example.com",
},
site := pubmodel.Site{
BaseURL: "https://example.com",
Posts: []*models.Post{
{
Meta: models.PostMeta{
Title: "Test Post",
Slug: "/2026/02/18/test-post",
},
Content: "This is a test post",
Title: "Test Post",
Slug: "/2026/02/18/test-post",
Body: "This is a test post",
},
{
Meta: models.PostMeta{
Title: "Another Post",
Slug: "/2026/02/20/another-post",
},
Content: "This is **another** test post",
Title: "Another Post",
Slug: "/2026/02/20/another-post",
Body: "This is **another** test post",
},
},
}
@ -64,4 +59,4 @@ func TestBuilder_BuildSite(t *testing.T) {
}
})
}
}

View file

@ -6,17 +6,17 @@ import (
"path/filepath"
"time"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/models/pubmodel"
)
func templateFns(site models.Site, opts Options) template.FuncMap {
func templateFns(site pubmodel.Site, opts Options) template.FuncMap {
return template.FuncMap{
"url_abs": func(basePath string) (string, error) {
if site.Meta.BaseURL == "" {
if site.BaseURL == "" {
return basePath, nil
}
pu, err := url.Parse(site.Meta.BaseURL)
pu, err := url.Parse(site.BaseURL)
if err != nil {
return "", err
}

View file

@ -6,6 +6,7 @@ import (
"time"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/models/pubmodel"
)
const (
@ -22,8 +23,6 @@ const (
)
type Options struct {
SiteMeta models.SiteMeta
// BasePosts is the base path for posts.
BasePosts string
@ -34,12 +33,12 @@ type Options struct {
}
type commonData struct {
Site models.SiteMeta
Site pubmodel.Site
}
type postSingleData struct {
commonData
Meta models.PostMeta
Meta *models.Post
HTML template.HTML
Path string
}

View file

@ -0,0 +1,27 @@
package sitereader
import (
"time"
"lmika.dev/lmika/weiro/models"
)
type ReadSiteModels struct {
Site models.Site
Target models.SitePublishTarget
Posts []*models.Post
}
type siteMeta struct {
Title string `yaml:"title"`
Tagline string `yaml:"tagline"`
BaseURL string `yaml:"base_url"`
}
type postMeta struct {
ID string `yaml:"id"`
Title string `yaml:"title"`
Date time.Time `yaml:"date"`
Tags []string `yaml:"tags"`
Slug string `yaml:"slug"`
}

View file

@ -4,6 +4,7 @@ import (
"bytes"
"io"
"io/fs"
"time"
"gopkg.in/yaml.v3"
"lmika.dev/lmika/weiro/models"
@ -19,24 +20,33 @@ func New(fs fs.FS) *Provider {
}
}
func (p *Provider) ReadSite() (models.Site, error) {
func (p *Provider) ReadSite() (ReadSiteModels, error) {
posts, err := p.ListPosts()
if err != nil {
return models.Site{}, err
return ReadSiteModels{}, err
}
meta := models.SiteMeta{}
meta := siteMeta{}
metaBytes, err := fs.ReadFile(p.fs, "site.yaml")
if err != nil {
return models.Site{}, err
return ReadSiteModels{}, err
}
if err := yaml.Unmarshal(metaBytes, &meta); err != nil {
return models.Site{}, err
return ReadSiteModels{}, err
}
return models.Site{
Meta: meta,
Posts: posts,
site := models.Site{
Title: meta.Title,
Tagline: meta.Tagline,
}
publishTarget := models.SitePublishTarget{
BaseURL: meta.BaseURL,
}
return ReadSiteModels{
Site: site,
Target: publishTarget,
Posts: posts,
}, nil
}
@ -70,13 +80,19 @@ func (p *Provider) ReadPost(path string) (*models.Post, error) {
return nil, io.ErrUnexpectedEOF
}
var meta models.PostMeta
var meta postMeta
if err := yaml.Unmarshal(parts[1], &meta); err != nil {
return nil, err
}
return &models.Post{
Meta: meta,
Content: string(bytes.TrimPrefix(parts[2], []byte("\n"))),
}, nil
post := models.Post{
Slug: meta.Slug,
Title: meta.Title,
GUID: meta.ID,
PublishedAt: meta.Date,
CreatedAt: time.Now(),
}
post.Body = string(bytes.TrimPrefix(parts[2], []byte("\n")))
return &post, nil
}

View file

@ -6,7 +6,6 @@ import (
"time"
"github.com/stretchr/testify/assert"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/providers/sitereader"
)
@ -27,10 +26,9 @@ This is just a test post.
post, err := pr.ReadPost("posts/test.md")
assert.NoError(t, err)
assert.Equal(t, "Test Post Here", post.Meta.Title)
assert.Equal(t, time.Date(2026, 2, 18, 19, 59, 0, 0, time.UTC), post.Meta.Date)
assert.Equal(t, []string{"test", "example"}, post.Meta.Tags)
assert.Equal(t, "This is just a test post.\n", post.Content)
assert.Equal(t, "Test Post Here", post.Title)
assert.Equal(t, time.Date(2026, 2, 18, 19, 59, 0, 0, time.UTC), post.PublishedAt)
assert.Equal(t, "This is just a test post.\n", post.Body)
})
t.Run("without meta", func(t *testing.T) {
@ -45,8 +43,8 @@ This is just a test post.
post, err := pr.ReadPost("posts/test.md")
assert.NoError(t, err)
assert.Equal(t, models.PostMeta{}, post.Meta)
assert.Equal(t, "This is just a test post.\n", post.Content)
assert.Equal(t, "", post.Title)
assert.Equal(t, "This is just a test post.\n", post.Body)
})
}
@ -74,12 +72,13 @@ This is just a test post.
assert.Equal(t, 2, len(posts))
assert.Equal(t, "111", posts[0].Meta.ID)
assert.Equal(t, "222", posts[1].Meta.ID)
assert.Equal(t, "111", posts[0].GUID)
assert.Equal(t, "222", posts[1].GUID)
}
func TestProvider_ReadSite(t *testing.T) {
testFS := fstest.MapFS{
"site.yaml": {Data: []byte(`base_url: https://example.com`)},
"posts/01-post1.md": {Data: []byte(`---
id: 111
date: 2026-02-18T19:59:00Z
@ -102,6 +101,6 @@ This is just a test post.
assert.Equal(t, 2, len(sites.Posts))
assert.Equal(t, "111", sites.Posts[0].Meta.ID)
assert.Equal(t, "222", sites.Posts[1].Meta.ID)
assert.Equal(t, "111", sites.Posts[0].GUID)
assert.Equal(t, "222", sites.Posts[1].GUID)
}