11package storage
22
33import (
4+ "context"
5+ "crypto/rand"
6+ "database/sql"
7+ "embed"
8+ "encoding/hex"
49 "fmt"
510
611 "github.com/calvinmclean/automated-garden/garden-app/pkg"
712 "github.com/calvinmclean/automated-garden/garden-app/pkg/notifications"
8- "github.com/calvinmclean/automated-garden/garden-app/pkg/storage/sql"
913 "github.com/calvinmclean/automated-garden/garden-app/pkg/weather"
10-
1114 "github.com/calvinmclean/babyapi"
15+ "github.com/golang-migrate/migrate/v4"
16+ "github.com/golang-migrate/migrate/v4/database/sqlite3"
17+ "github.com/golang-migrate/migrate/v4/source/iofs"
18+ "github.com/rs/xid"
19+
20+ // sqlite driver import
21+ _ "modernc.org/sqlite"
1222)
1323
14- // Config is used to configure the SQLite storage client
24+ //go:generate sqlc generate
25+
26+ //go:embed migrations/*.sql
27+ var migrationsFS embed.FS
28+
29+ // Config holds configuration for the storage backend
1530type Config struct {
1631 ConnectionString string `mapstructure:"connection_string" yaml:"connection_string"`
1732}
1833
19- // AdditionalQueries are queries that are implemented outside of the base babyapi implementations
20- type AdditionalQueries interface {
21- GetZonesUsingWaterSchedule (id string ) ([]* pkg.ZoneAndGarden , error )
22- GetWaterSchedulesUsingWeatherClient (id string ) ([]* pkg.WaterSchedule , error )
23- }
24-
2534type Client struct {
2635 Gardens babyapi.Storage [* pkg.Garden ]
2736 Zones babyapi.Storage [* pkg.Zone ]
@@ -30,24 +39,83 @@ type Client struct {
3039 NotificationClientConfigs babyapi.Storage [* notifications.Client ]
3140 WaterRoutines babyapi.Storage [* pkg.WaterRoutine ]
3241
33- AdditionalQueries
42+ * AdditionalQueries
3443}
3544
45+ // NewClient creates a new storage.Client using SQL backend.
46+ // It initializes the database connection using the provided config.
3647func NewClient (config Config ) (* Client , error ) {
37- sqlClient , err := sql .NewClient (sql.Config {
38- DataSourceName : config .ConnectionString ,
39- })
48+ connectionString := config .ConnectionString
49+ // Use shared cache for in-memory databases to allow multiple connections to share the same database
50+ // Generate a unique database name for each client so tests don't interfere with each other
51+ if connectionString == ":memory:" {
52+ // Generate a random identifier for the database name
53+ randomBytes := make ([]byte , 8 )
54+ if _ , err := rand .Read (randomBytes ); err != nil {
55+ return nil , fmt .Errorf ("error generating random database name: %w" , err )
56+ }
57+ randomName := hex .EncodeToString (randomBytes )
58+ connectionString = fmt .Sprintf ("file:mem%s?mode=memory&cache=shared" , randomName )
59+ }
60+
61+ db , err := sql .Open ("sqlite" , connectionString )
4062 if err != nil {
41- return nil , fmt .Errorf ("error creating SQL client: %w" , err )
63+ return nil , fmt .Errorf ("error opening sqlite database: %w" , err )
64+ }
65+
66+ err = runMigrations (db )
67+ if err != nil {
68+ return nil , fmt .Errorf ("error running migrations: %w" , err )
4269 }
4370
4471 return & Client {
45- Gardens : sqlClient . Gardens ,
46- Zones : sqlClient . Zones ,
47- WaterSchedules : sqlClient . WaterSchedules ,
48- WeatherClientConfigs : sqlClient . WeatherClientConfigs ,
49- NotificationClientConfigs : sqlClient . NotificationClientConfigs ,
50- WaterRoutines : sqlClient . WaterRoutines ,
51- AdditionalQueries : sqlClient . AdditionalQueries ,
72+ Gardens : NewGardenStorage ( db ) ,
73+ Zones : NewZoneStorage ( db ) ,
74+ WaterSchedules : NewWaterScheduleStorage ( db ) ,
75+ WeatherClientConfigs : NewWeatherClientStorage ( db ) ,
76+ NotificationClientConfigs : NewNotificationClientStorage ( db ) ,
77+ WaterRoutines : NewWaterRoutineStorage ( db ) ,
78+ AdditionalQueries : NewAdditionalQueries ( db ) ,
5279 }, nil
5380}
81+
82+ // GetWeatherClient retrieves a WeatherClient by ID and initializes it
83+ func (c * Client ) GetWeatherClient (id xid.ID ) (weather.Client , error ) {
84+ clientConfig , err := c .WeatherClientConfigs .Get (context .Background (), id .String ())
85+ if err != nil {
86+ return nil , fmt .Errorf ("error getting weather client config: %w" , err )
87+ }
88+
89+ if clientConfig == nil {
90+ return nil , fmt .Errorf ("weather client config not found" )
91+ }
92+
93+ return weather .NewClient (clientConfig , func (weatherClientOptions map [string ]any ) error {
94+ clientConfig .Options = weatherClientOptions
95+ return c .WeatherClientConfigs .Set (context .Background (), clientConfig )
96+ })
97+ }
98+
99+ // runMigrations executes all pending database migrations
100+ func runMigrations (db * sql.DB ) error {
101+ driver , err := sqlite3 .WithInstance (db , & sqlite3.Config {})
102+ if err != nil {
103+ return fmt .Errorf ("error creating migration driver: %w" , err )
104+ }
105+
106+ migrations , err := iofs .New (migrationsFS , "migrations" )
107+ if err != nil {
108+ return fmt .Errorf ("error creating migration source: %w" , err )
109+ }
110+
111+ m , err := migrate .NewWithInstance ("iofs" , migrations , "sqlite3" , driver )
112+ if err != nil {
113+ return fmt .Errorf ("error creating migration instance: %w" , err )
114+ }
115+
116+ if err := m .Up (); err != nil && err != migrate .ErrNoChange {
117+ return fmt .Errorf ("error running migrations: %w" , err )
118+ }
119+
120+ return nil
121+ }
0 commit comments