You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
4.5 KiB
Go
204 lines
4.5 KiB
Go
package dbtimesprovider
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"database/sql"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/doug-martin/goqu/v9"
|
|
"github.com/samber/lo"
|
|
|
|
"prayertimes/pkg/prayer"
|
|
)
|
|
|
|
//go:embed schema.sql
|
|
var schema string
|
|
|
|
//go:embed locations.jsonl
|
|
var locationsJSON string
|
|
|
|
type Provider struct {
|
|
db *goqu.Database
|
|
provider prayer.TimesProvider
|
|
clockFunc func() time.Time
|
|
}
|
|
|
|
func New(db *goqu.Database, provider prayer.TimesProvider) Provider {
|
|
return Provider{
|
|
provider: provider,
|
|
clockFunc: time.Now,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
func (p Provider) Name() string {
|
|
return "db:" + p.provider.Name()
|
|
}
|
|
|
|
func (p Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
|
|
times, err := p.loadTimes(ctx, location)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load prayer times from db: %w", err)
|
|
}
|
|
if len(times) > 0 {
|
|
return times, nil
|
|
}
|
|
|
|
times, err = p.provider.Get(ctx, location)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get prayer times: %w", err)
|
|
}
|
|
|
|
if len(times) > 0 {
|
|
if err := p.saveTimes(ctx, location, times); err != nil {
|
|
return nil, fmt.Errorf("failed to save times to db: %w", err)
|
|
}
|
|
}
|
|
|
|
return times, nil
|
|
}
|
|
|
|
func Migrate(con *sql.DB) error {
|
|
db := goqu.New("sqlite3", con)
|
|
if _, err := db.Exec(schema); err != nil {
|
|
return fmt.Errorf("failed to migrate: %w", err)
|
|
}
|
|
|
|
count, _ := db.From("locations").Count()
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
|
|
type entry struct {
|
|
ID int `json:"id" db:"id"`
|
|
Country string `json:"country" db:"country"`
|
|
Region string `json:"region" db:"region"`
|
|
City string `json:"city" db:"city"`
|
|
}
|
|
s := bufio.NewScanner(strings.NewReader(locationsJSON))
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin tx: %w", err)
|
|
}
|
|
if err := tx.Wrap(func() error {
|
|
for s.Scan() {
|
|
var e entry
|
|
if err := json.Unmarshal(s.Bytes(), &e); err != nil {
|
|
return fmt.Errorf("failed to parse as json: %w", err)
|
|
}
|
|
|
|
q := tx.Insert("locations").
|
|
OnConflict(goqu.DoNothing()).
|
|
Rows(e)
|
|
|
|
if _, err := q.Executor().Exec(); err != nil {
|
|
return fmt.Errorf("failed to insert location: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type prayerTimesRow struct {
|
|
ProviderID int64 `db:"provider_id"`
|
|
LocationID string `db:"location_id"`
|
|
Date string `db:"date"`
|
|
Fajr string `db:"fajr"`
|
|
Sunrise string `db:"sunrise"`
|
|
Dhuhr string `db:"dhuhr"`
|
|
Asr string `db:"asr"`
|
|
Maghrib string `db:"maghrib"`
|
|
Isha string `db:"isha"`
|
|
}
|
|
|
|
func (r prayerTimesRow) toDomain() prayer.Times {
|
|
return prayer.Times{
|
|
Date: r.Date,
|
|
Fajr: r.Fajr,
|
|
Sunrise: r.Sunrise,
|
|
Dhuhr: r.Dhuhr,
|
|
Asr: r.Asr,
|
|
Maghrib: r.Maghrib,
|
|
Isha: r.Isha,
|
|
}
|
|
}
|
|
|
|
func (p Provider) saveTimes(ctx context.Context, locationID string, times []prayer.Times) error {
|
|
providerID, err := p.saveProvider(ctx, p.provider.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := lo.Map(times, func(item prayer.Times, _ int) prayerTimesRow {
|
|
return prayerTimesRow{
|
|
ProviderID: providerID,
|
|
LocationID: locationID,
|
|
Date: item.Date,
|
|
Fajr: item.Fajr,
|
|
Sunrise: item.Sunrise,
|
|
Dhuhr: item.Dhuhr,
|
|
Asr: item.Asr,
|
|
Maghrib: item.Maghrib,
|
|
Isha: item.Isha,
|
|
}
|
|
})
|
|
q := p.db.
|
|
Insert("prayer_times").
|
|
OnConflict(goqu.DoNothing()).
|
|
Rows(rows)
|
|
|
|
if _, err := q.Executor().ExecContext(ctx); err != nil {
|
|
return fmt.Errorf("failed to save times: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p Provider) loadTimes(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
|
now := p.clockFunc()
|
|
today := now.UTC().Truncate(time.Hour * 24)
|
|
|
|
q := p.db.
|
|
From(goqu.T("prayer_times").As("pt")).
|
|
Join(goqu.T("providers").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("pt.provider_id")))).
|
|
Where(
|
|
goqu.I("p.name").Eq(p.provider.Name()),
|
|
goqu.I("pt.location_id").Eq(locationID),
|
|
goqu.I("pt.date").Gte(today),
|
|
).
|
|
Limit(100)
|
|
|
|
var rows []prayerTimesRow
|
|
if err := q.ScanStructsContext(ctx, &rows); err != nil {
|
|
return nil, fmt.Errorf("failed to scan times: %w", err)
|
|
}
|
|
|
|
return lo.Map(rows, func(row prayerTimesRow, _ int) prayer.Times {
|
|
return row.toDomain()
|
|
}), nil
|
|
}
|
|
|
|
func (p Provider) saveProvider(ctx context.Context, name string) (int64, error) {
|
|
q := p.db.Insert("providers").
|
|
OnConflict(goqu.DoUpdate("name", goqu.Record{"name": name})).
|
|
Rows(goqu.Record{"name": name}).
|
|
Returning("id")
|
|
var id int64
|
|
_, err := q.Executor().ScanValContext(ctx, &id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to insert provider: %w", err)
|
|
}
|
|
return id, nil
|
|
}
|