feeds tests pass
parent
d77e35596e
commit
a2d1d17e23
|
|
@ -2,9 +2,8 @@ package cron
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"show-rss/src/db"
|
||||
"strings"
|
||||
"io"
|
||||
"show-rss/src/feeds"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -25,71 +24,10 @@ func Main(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func One(ctx context.Context) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
return fmt.Errorf("failed init db: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
if err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS database_version (v NUMBER, t TIMESTAMP)`); err != nil {
|
||||
return fmt.Errorf("failed to create database_version table: %w", err)
|
||||
}
|
||||
|
||||
type DatabaseVersion struct {
|
||||
V int `json:"v"`
|
||||
T time.Time `json:"t"`
|
||||
}
|
||||
vs, err := db.Query[DatabaseVersion](ctx, `SELECT v, t FROM database_version ORDER BY v DESC LIMIT 1`)
|
||||
feeds, err := feeds.New(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v DatabaseVersion
|
||||
if len(vs) > 0 {
|
||||
v = vs[0]
|
||||
}
|
||||
|
||||
mods := []string{
|
||||
`CREATE TABLE "feed.entries" (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
deleted_at TIMESTAMP
|
||||
)`,
|
||||
|
||||
`CREATE TABLE "feed.versions" (
|
||||
entries_id NUMBER NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (entries_id, created_at),
|
||||
FOREIGN KEY (entries_id) REFERENCES "feed.entries" (id)
|
||||
)`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`,
|
||||
|
||||
`CREATE TABLE "feed.current_versions" (
|
||||
entries_id NUMBER NOT NULL,
|
||||
versions_created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at)
|
||||
)`,
|
||||
|
||||
`CREATE TABLE "feed.executions" (
|
||||
entries_id NUMBER,
|
||||
versions_created_at TIMESTAMP NOT NULL,
|
||||
executed_at TIMESTAMP,
|
||||
FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at)
|
||||
)`,
|
||||
}
|
||||
mods = append([]string{""}, mods...)
|
||||
for i := v.V + 1; i < len(mods); i++ {
|
||||
q := mods[i]
|
||||
q = strings.TrimSpace(q)
|
||||
q = strings.TrimSuffix(q, ";")
|
||||
q = fmt.Sprintf("BEGIN; %s; INSERT INTO database_version (v, t) VALUES (?, ?); COMMIT;", q)
|
||||
if err := db.Exec(ctx, q, i, time.Now()); err != nil {
|
||||
return fmt.Errorf("[%d] failed mod %s: %w", i, mods[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return io.EOF
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const ctxKey = "__db"
|
|||
|
||||
func Test(t *testing.T, ctx context.Context) context.Context {
|
||||
p := path.Join(t.TempDir(), strings.ReplaceAll(t.Name()+".db", "/", "_"))
|
||||
t.Logf("test db @ %s", p)
|
||||
ctx, err := Inject(ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to inject db %s: %v", p, err)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
package feeds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"show-rss/src/db"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Feeds struct{}
|
||||
|
||||
func New(ctx context.Context) (Feeds, error) {
|
||||
return Feeds{}, initDB(ctx)
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
if err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS database_version (v NUMBER, t TIMESTAMP)`); err != nil {
|
||||
return fmt.Errorf("failed to create database_version table: %w", err)
|
||||
}
|
||||
|
||||
type DatabaseVersion struct {
|
||||
V int `json:"v"`
|
||||
T time.Time `json:"t"`
|
||||
}
|
||||
vs, err := db.Query[DatabaseVersion](ctx, `SELECT v, t FROM database_version ORDER BY v DESC LIMIT 1`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v DatabaseVersion
|
||||
if len(vs) > 0 {
|
||||
v = vs[0]
|
||||
}
|
||||
|
||||
mods := []string{
|
||||
`CREATE TABLE "feed.entries" (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
deleted_at TIMESTAMP
|
||||
)`,
|
||||
|
||||
`CREATE TABLE "feed.versions" (
|
||||
entries_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (entries_id, created_at),
|
||||
FOREIGN KEY (entries_id) REFERENCES "feed.entries" (id)
|
||||
)`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`,
|
||||
`ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`,
|
||||
|
||||
`CREATE TABLE "feed.current_versions" (
|
||||
entries_id TEXT NOT NULL,
|
||||
versions_created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at)
|
||||
)`,
|
||||
|
||||
`CREATE TABLE "feed.executions" (
|
||||
entries_id TEXT,
|
||||
versions_created_at TIMESTAMP NOT NULL,
|
||||
executed_at TIMESTAMP,
|
||||
FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at)
|
||||
)`,
|
||||
}
|
||||
mods = append([]string{""}, mods...)
|
||||
for i := v.V + 1; i < len(mods); i++ {
|
||||
q := mods[i]
|
||||
q = strings.TrimSpace(q)
|
||||
q = strings.TrimSuffix(q, ";")
|
||||
q = fmt.Sprintf("BEGIN; %s; INSERT INTO database_version (v, t) VALUES (?, ?); COMMIT;", q)
|
||||
if err := db.Exec(ctx, q, i, time.Now()); err != nil {
|
||||
return fmt.Errorf("[%d] failed mod %s: %w", i, mods[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
Feed struct {
|
||||
Entry Entry
|
||||
Version Version
|
||||
Execution Execution
|
||||
}
|
||||
|
||||
Entry struct {
|
||||
ID string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Deleted time.Time
|
||||
}
|
||||
|
||||
Version struct {
|
||||
Created time.Time
|
||||
URL string
|
||||
Cron string
|
||||
}
|
||||
|
||||
Execution struct {
|
||||
Executed time.Time
|
||||
VersionCreated time.Time
|
||||
}
|
||||
)
|
||||
|
||||
func (f *Feeds) Get(ctx context.Context, id string) (Feed, error) {
|
||||
entry, err := f.getEntry(ctx, id)
|
||||
if err != nil {
|
||||
return Feed{}, err
|
||||
}
|
||||
|
||||
version, err := db.QueryOne[Version](ctx, `
|
||||
SELECT
|
||||
"feed.current_versions".versions_created_at AS Created,
|
||||
"feed.current_versions" AS URL,
|
||||
"feed.current_versions" AS Cron
|
||||
FROM
|
||||
"feed.current_versions"
|
||||
JOIN
|
||||
"feed.versions" versions_a ON
|
||||
"feed.current_versions".entries_id=versions_a.entries_id
|
||||
JOIN
|
||||
"feed.versions" versions_b ON
|
||||
"feed.current_versions".versions_created_at=versions_b.created_at
|
||||
WHERE
|
||||
"feed.current_versions".entries_id = ?
|
||||
`, id)
|
||||
if err != nil {
|
||||
return Feed{}, err
|
||||
}
|
||||
|
||||
execution, err := db.QueryOne[Execution](ctx, `
|
||||
SELECT
|
||||
"feed.executed_at" AS Executed,
|
||||
"feed.versions_created_at" AS VersionsCreated
|
||||
FROM
|
||||
"feed.executions"
|
||||
WHERE
|
||||
"feed.executions".entries_id = ?
|
||||
ORDER BY "feed.executions".executed_at DESC
|
||||
`, id)
|
||||
if err != nil {
|
||||
return Feed{}, err
|
||||
}
|
||||
|
||||
return Feed{}, fmt.Errorf("%+v, %+v, %+v", entry, version, execution)
|
||||
}
|
||||
|
||||
func (f *Feeds) Insert(ctx context.Context, url, cron string) (string, error) {
|
||||
now := time.Now()
|
||||
id := uuid.New().String()
|
||||
q := `
|
||||
BEGIN;
|
||||
INSERT INTO "feed.entries" (
|
||||
id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?);
|
||||
INSERT INTO "feed.versions" (
|
||||
entries_id,
|
||||
created_at,
|
||||
url,
|
||||
cron
|
||||
) VALUES (?, ?, ?, ?);
|
||||
INSERT INTO "feed.current_versions" (
|
||||
entries_id,
|
||||
versions_created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?);
|
||||
COMMIT;
|
||||
`
|
||||
return id, db.Exec(ctx, q,
|
||||
id, now, now,
|
||||
id, now, url, cron,
|
||||
id, now, now,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *Feeds) Update(ctx context.Context, id string, url, cron *string) error {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (f *Feeds) Delete(ctx context.Context, id string) error {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (f *Feeds) getEntry(ctx context.Context, id string) (Entry, error) {
|
||||
return db.QueryOne[Entry](ctx, `
|
||||
SELECT
|
||||
id AS ID,
|
||||
created_at AS Created,
|
||||
updated_at AS Updated,
|
||||
deleted_at AS Deleted
|
||||
FROM
|
||||
"feed.entries"
|
||||
WHERE
|
||||
id = ?
|
||||
`, id)
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package feeds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"show-rss/src/db"
|
||||
"show-rss/src/feeds"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFeeds(t *testing.T) {
|
||||
ctx, can := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer can()
|
||||
|
||||
t.Run("same ctx", func(t *testing.T) {
|
||||
ctx := db.Test(t, ctx)
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
if _, err := feeds.New(ctx); err != nil && ctx.Err() == nil {
|
||||
t.Fatalf("failed %d: %v", i, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new ctx", func(t *testing.T) {
|
||||
for i := 0; i < 2; i++ {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
if _, err := feeds.New(db.Test(t, ctx)); err != nil && ctx.Err() == nil {
|
||||
t.Fatalf("failed %d: %v", i, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("crud", func(t *testing.T) {
|
||||
ctx := db.Test(t, ctx)
|
||||
|
||||
f, err := feeds.New(ctx)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
t.Fatalf("failed: %v", err)
|
||||
}
|
||||
|
||||
id, err := f.Insert(ctx, "url", "cron")
|
||||
if err != nil {
|
||||
t.Fatal("cannot insert:", err)
|
||||
}
|
||||
|
||||
got, err := f.Get(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal("cannot get:", err)
|
||||
}
|
||||
t.Errorf("%+v", got)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue