feat(api): Add compatibility layer for API v1

master
Abdussamet Kocak 4 weeks ago
parent 82f252f32f
commit ad1a9364b8

@ -28,6 +28,7 @@ type httpError struct {
type Services struct { type Services struct {
PrayerProvider PrayerProvider PrayerProvider PrayerProvider
LocationProvider LocationProvider LocationProvider LocationProvider
LegacyLocationProvider LegacyLocationProvider
} }
type PrayerProvider interface { type PrayerProvider interface {
@ -40,6 +41,11 @@ type LocationProvider interface {
SearchLocationsByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Location, error) SearchLocationsByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Location, error)
} }
type LegacyLocationProvider interface {
SearchLocations(ctx context.Context, query string) ([]prayer.LegacyLocation, error)
GetLocationByID(ctx context.Context, id int) (prayer.LegacyLocation, error)
}
func New(services Services) *fiber.App { func New(services Services) *fiber.App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Immutable: true, Immutable: true,
@ -192,6 +198,74 @@ func New(services Services) *fiber.App {
}) })
}) })
app.Get("/api/diyanet/search", func(ctx fiber.Ctx) error {
q := strings.TrimSpace(ctx.Query("q"))
if q == "" {
return fmt.Errorf("missing query parameter q: %w", fiber.ErrBadRequest)
}
locations, err := services.LegacyLocationProvider.SearchLocations(ctx.Context(), q)
if err != nil {
return fmt.Errorf("failed to search legacy diyanet locations: %w", err)
}
resp := make([]fiber.Map, 0, len(locations))
for _, location := range locations {
resp = append(resp, fiber.Map{
"id": location.ID,
"country": location.Country,
"city": location.City,
"region": location.Region,
})
}
ctx.Response().Header.Set(fiber.HeaderCacheControl, "max-age=86400")
return ctx.JSON(resp)
})
app.Get("/api/diyanet/prayertimes", func(ctx fiber.Ctx) error {
if services.LegacyLocationProvider == nil {
return fmt.Errorf("legacy location provider is not configured: %w", fiber.ErrNotFound)
}
locationIDText := strings.TrimSpace(ctx.Query("location_id"))
if locationIDText == "" {
return fmt.Errorf("missing query parameter location_id: %w", fiber.ErrBadRequest)
}
locationID, err := strconv.Atoi(locationIDText)
if err != nil {
return fmt.Errorf("failed to parse location_id query parameter: %w", errors.Join(fiber.ErrBadRequest, err))
}
legacyLocation, err := services.LegacyLocationProvider.GetLocationByID(ctx.Context(), locationID)
if err != nil {
return fmt.Errorf("failed to resolve location by location_id: %w", errors.Join(fiber.ErrNotFound, err))
}
result, err := services.PrayerProvider.GetByCoords(ctx.Context(), prayer.Coordinates{
Latitude: legacyLocation.Latitude,
Longitude: legacyLocation.Longitude,
})
if err != nil {
return fmt.Errorf("failed to fetch prayer times for location_id: %w", err)
}
location := result.Location
location.ID = legacyLocation.ID
if strings.TrimSpace(legacyLocation.Timezone) != "" {
location.Timezone = legacyLocation.Timezone
}
mapped, err := mapPrayerTimesForLegacyResponse(result.Times, location)
if err != nil {
return fmt.Errorf("failed to map legacy prayer times response: %w", err)
}
ctx.Response().Header.Set(fiber.HeaderCacheControl, "max-age=86400")
return ctx.JSON(mapped)
})
app.Use("/", static.New("", static.Config{FS: templates.FS()})) app.Use("/", static.New("", static.Config{FS: templates.FS()}))
return app return app
@ -293,3 +367,29 @@ func loadTimezone(name string) (*time.Location, error) {
return loc, nil return loc, nil
} }
func mapPrayerTimesForLegacyResponse(times []prayer.Times, location prayer.Location) ([]fiber.Map, error) {
tz := time.UTC
if strings.TrimSpace(location.Timezone) != "" {
loadedTZ, err := loadTimezone(location.Timezone)
if err != nil {
return nil, fmt.Errorf("failed to load location timezone: %w", err)
}
tz = loadedTZ
}
out := make([]fiber.Map, 0, len(times))
for _, item := range times {
out = append(out, fiber.Map{
"date": item.Date.In(tz).Format(time.RFC3339),
"fajr": formatHHMM(item.Fajr, tz),
"sun": formatHHMM(item.Sunrise, tz),
"dhuhr": formatHHMM(item.Dhuhr, tz),
"asr": formatHHMM(item.Asr, tz),
"maghrib": formatHHMM(item.Maghrib, tz),
"isha": formatHHMM(item.Isha, tz),
})
}
return out, nil
}

@ -1,17 +1,21 @@
package main package main
import ( import (
"database/sql"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
_ "modernc.org/sqlite"
"prayertimes/internal/api" "prayertimes/internal/api"
"prayertimes/internal/net" "prayertimes/internal/net"
"prayertimes/pkg/citydb" "prayertimes/pkg/citydb"
"prayertimes/pkg/diyanetapi" "prayertimes/pkg/diyanetapi"
"prayertimes/pkg/diyanetcalc" "prayertimes/pkg/diyanetcalc"
"prayertimes/pkg/diyanetcitydb"
"prayertimes/pkg/diyanetprovider" "prayertimes/pkg/diyanetprovider"
) )
@ -28,11 +32,14 @@ func run() error {
return fmt.Errorf("DB_PATH is not set") return fmt.Errorf("DB_PATH is not set")
} }
locationProvider, err := citydb.Open(dbPath) conn, err := newDB(dbPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to open city database: %w", err) return fmt.Errorf("failed to initialize database connection: %w", err)
} }
defer locationProvider.Close() defer conn.Close()
locationProvider := citydb.NewWithConn(conn)
legacyLocationProvider := diyanetcitydb.New(conn)
diyanetAPIProvider := diyanetapi.New(net.ReqClient) diyanetAPIProvider := diyanetapi.New(net.ReqClient)
diyanetCalcProvider := diyanetcalc.New() diyanetCalcProvider := diyanetcalc.New()
@ -41,6 +48,7 @@ func run() error {
services := api.Services{ services := api.Services{
PrayerProvider: prayerProvider, PrayerProvider: prayerProvider,
LocationProvider: locationProvider, LocationProvider: locationProvider,
LegacyLocationProvider: legacyLocationProvider,
} }
app := api.New(services) app := api.New(services)
@ -58,3 +66,17 @@ func getDefaultEnv(name string, defaultValue string) string {
} }
return v return v
} }
func newDB(path string) (*sql.DB, error) {
conn, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
if err := conn.Ping(); err != nil {
_ = conn.Close()
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
return conn, nil
}

@ -15,10 +15,15 @@ import (
type Provider struct { type Provider struct {
db *goqu.Database db *goqu.Database
own bool
} }
func New(db *goqu.Database) Provider { func New(db *goqu.Database) Provider {
return Provider{db: db} return Provider{db: db, own: false}
}
func NewWithConn(conn *sql.DB) Provider {
return Provider{db: goqu.New("sqlite3", conn), own: false}
} }
func Open(path string) (Provider, error) { func Open(path string) (Provider, error) {
@ -31,10 +36,14 @@ func Open(path string) (Provider, error) {
return Provider{}, fmt.Errorf("failed to connect to cities database: %w", err) return Provider{}, fmt.Errorf("failed to connect to cities database: %w", err)
} }
return New(goqu.New("sqlite3", conn)), nil return Provider{db: goqu.New("sqlite3", conn), own: true}, nil
} }
func (p Provider) Close() error { func (p Provider) Close() error {
if !p.own {
return nil
}
if err := p.db.Db.(*sql.DB).Close(); err != nil { if err := p.db.Db.(*sql.DB).Close(); err != nil {
return fmt.Errorf("failed to close cities database: %w", err) return fmt.Errorf("failed to close cities database: %w", err)
} }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,125 @@
package diyanetcitydb
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"github.com/doug-martin/goqu/v9"
"prayertimes/pkg/prayer"
)
var ErrLocationNotFound = errors.New("location not found")
type Provider struct {
db *goqu.Database
}
func New(conn *sql.DB) Provider {
return Provider{db: goqu.New("sqlite3", conn)}
}
type locationRow struct {
ID int `db:"id"`
Country string `db:"country"`
City string `db:"city"`
Region string `db:"region"`
Latitude float64 `db:"latitude"`
Longitude float64 `db:"longitude"`
Timezone string `db:"timezone"`
}
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.LegacyLocation, error) {
query = strings.TrimSpace(query)
if query == "" {
return nil, nil
}
pattern := "%" + query + "%"
q := p.db.
From("diyanet_cities").
Select(
goqu.I("id"),
goqu.I("country"),
goqu.I("city"),
goqu.COALESCE(goqu.I("region"), "").As("region"),
goqu.I("latitude"),
goqu.I("longitude"),
goqu.COALESCE(goqu.I("timezone"), "").As("timezone"),
).
Where(
goqu.Or(
goqu.L("country LIKE ? COLLATE NOCASE", pattern),
goqu.L("city LIKE ? COLLATE NOCASE", pattern),
goqu.L("COALESCE(region, '') LIKE ? COLLATE NOCASE", pattern),
),
).
Order(
goqu.I("country").Asc(),
goqu.I("city").Asc(),
goqu.I("region").Asc(),
goqu.I("id").Asc(),
).
Limit(100)
rows := make([]locationRow, 0)
if err := q.ScanStructsContext(ctx, &rows); err != nil {
return nil, fmt.Errorf("failed to query diyanet locations: %w", err)
}
locations := make([]prayer.LegacyLocation, 0, len(rows))
for _, row := range rows {
locations = append(locations, prayer.LegacyLocation{
ID: row.ID,
Country: row.Country,
City: row.City,
Region: row.Region,
Latitude: row.Latitude,
Longitude: row.Longitude,
Timezone: row.Timezone,
})
}
return locations, nil
}
func (p Provider) GetLocationByID(ctx context.Context, id int) (prayer.LegacyLocation, error) {
q := p.db.
From("diyanet_cities").
Select(
goqu.I("id"),
goqu.I("country"),
goqu.I("city"),
goqu.COALESCE(goqu.I("region"), "").As("region"),
goqu.I("latitude"),
goqu.I("longitude"),
goqu.COALESCE(goqu.I("timezone"), "").As("timezone"),
).
Where(goqu.I("id").Eq(id)).
Limit(1)
var rows []locationRow
if err := q.ScanStructsContext(ctx, &rows); err != nil {
return prayer.LegacyLocation{}, fmt.Errorf("failed to query location by id: %w", err)
}
if len(rows) == 0 {
return prayer.LegacyLocation{}, ErrLocationNotFound
}
row := rows[0]
location := prayer.LegacyLocation{
ID: row.ID,
Country: row.Country,
City: row.City,
Region: row.Region,
Latitude: row.Latitude,
Longitude: row.Longitude,
Timezone: row.Timezone,
}
return location, nil
}

@ -39,3 +39,13 @@ type TimesResult struct {
Location Location `json:"location"` Location Location `json:"location"`
Times []Times `json:"prayertimes"` Times []Times `json:"prayertimes"`
} }
type LegacyLocation struct {
ID int
Country string
City string
Region string
Latitude float64
Longitude float64
Timezone string
}

Loading…
Cancel
Save