diff --git a/CQRS/Commands/SaveWeatherCommand.cs b/CQRS/Commands/SaveWeatherCommand.cs index a8433652..42bc59d3 100644 --- a/CQRS/Commands/SaveWeatherCommand.cs +++ b/CQRS/Commands/SaveWeatherCommand.cs @@ -6,4 +6,5 @@ namespace Gridly.Commands; public class SaveWeatherCommand : IRequest { public WeatherDataModel Weather { get; set; } + public int CardId { get; set; } } \ No newline at end of file diff --git a/Constants/QueryStrings.cs b/Constants/QueryStrings.cs index 4d709867..4a5a4763 100644 --- a/Constants/QueryStrings.cs +++ b/Constants/QueryStrings.cs @@ -27,11 +27,32 @@ INSERT INTO IconsConnected (CardId, IconId) VALUES (@CardId, @IconId); SELECT * FROM IconsConnected WHERE Id = last_insert_rowid();"; - public const string InsertWeatherDataQuery = @" - INSERT INTO WeatherData (CardId, Address, Timezone, Description, Temp, FeelsLike, Humidity, WindSpeed, WindDir, FetchedAt) - VALUES (@CardId, @Address, @Timezone, @Description, @Temp, @FeelsLike, @Humidity, @WindSpeed, @WindDir, @FetchedAt); - SELECT * FROM RowColumn WHERE Id = last_insert_rowid();"; - + public const string UpsertWeatherDataQuery = @" + INSERT INTO WeatherData (Address, Timezone, Description, Temp, FeelsLike, Humidity, WindSpeed, WindDir, FetchedAt) + VALUES (@Address, @Timezone, @Description, @Temp, @FeelsLike, @Humidity, @WindSpeed, @WindDir, @FetchedAt) + ON CONFLICT(Address) DO UPDATE SET + Timezone = excluded.Timezone, + Description = excluded.Description, + Temp = excluded.Temp, + FeelsLike = excluded.FeelsLike, + Humidity = excluded.Humidity, + WindSpeed = excluded.WindSpeed, + WindDir = excluded.WindDir, + FetchedAt = excluded.FetchedAt + RETURNING *;"; + + public const string UpsertWeatherDataConnectionQuery = @" + INSERT INTO WeatherDataConnection (CardId, WeatherId) + VALUES (@CardId, @WeatherId) + ON CONFLICT(CardId) DO UPDATE SET + WeatherId = excluded.WeatherId + RETURNING *;"; + + public const string DeleteOrphanedWeatherDataQuery = @" + DELETE FROM WeatherData + WHERE Id = @Id + AND NOT EXISTS (SELECT 1 FROM WeatherDataConnection WHERE WeatherId = @Id);"; + public const string SelectCardQuery = @" SELECT co.Id AS CardId, @@ -141,23 +162,22 @@ UPDATE ProviderKeys FROM ProviderKeys WHERE Provider = @Provider;"; - public const string UpdateWeatherDataQuery = @" - UPDATE WeatherData - SET Address = @Address, - Timezone = @Timezone, - Description = @Description, - Temp = @Temp, - FeelsLike = @FeelsLike, - Humidity = @Humidity, - WindSpeed = @WindSpeed, - WindDir = @WindDir, - FetchedAt = @FetchedAt - WHERE CardId = @CardId;"; - public const string SelectWeatherDataQuery = @" - SELECT Id, CardId, Address, Timezone, Description, Temp, FeelsLike, Humidity, WindSpeed, WindDir, FetchedAt + SELECT Id, Address, Timezone, Description, Temp, FeelsLike, Humidity, WindSpeed, WindDir, FetchedAt FROM WeatherData /**where**/"; + public const string SelectAllWeatherDataQuery = @" + SELECT wc.CardId, w.Id AS WeatherId, w.Address, w.Timezone, w.Description, + w.Temp, w.FeelsLike, w.Humidity, w.WindSpeed, w.WindDir, w.FetchedAt + FROM WeatherDataConnection wc + INNER JOIN WeatherData w ON w.Id = wc.WeatherId /**where**/"; + + public const string SelectWeatherDataConnectionQuery = @" + SELECT * + FROM WeatherDataConnection wc /**where**/"; + + public const string DeleteFromWeatherDataConnectionQuery = "DELETE FROM WeatherDataConnection /**where**/"; + public const string JoinIconDataQuery = "Icon i ON i.Id = ic.IconId"; public const string JoinIconsConnectedDataQuery = "IconsConnected ic ON ic.CardId = co.Id"; public const string JoinSettingsQuery = "Settings cs ON cs.CardId = co.Id"; @@ -166,6 +186,8 @@ UPDATE WeatherData public const string WhereCardIdForeignKeyEqualId = "CardId = @CardId"; public const string WhereLocationEqualsLocation = "Address = @Address"; public const string WhereIdEqualsId = "Id = @Id"; + public const string WhereWeatherConnectedCardIdForeignKeyEqualIdWithAlias = "wc.CardId = @CardId"; + public const string WhereWeatherConnectedWeatherIdForeignKeyEqualIdWithAlias = "wc.WeatherId = @WeatherId"; public const string WhereIconConnectedIconIdForeignKeyEqualIdWithAlias = "ic.IconId = @IconId"; public const string WhereIconConnectedCardIdForeignKeyEqualIdWithAlias = "ic.CardId = @CardId"; public const string WhereCardIdEqualsCardIdWithAlias = "co.Id = @cardId"; diff --git a/Data/DbInitializer.cs b/Data/DbInitializer.cs index d5c72409..1d5ae57e 100644 --- a/Data/DbInitializer.cs +++ b/Data/DbInitializer.cs @@ -1,5 +1,6 @@ using Dapper; using System.Data; +using Microsoft.Data.Sqlite; namespace Gridly.Data; @@ -11,7 +12,7 @@ public DbInitializer(IDbConnection connection) { this.connection = connection; } - + public async Task EnsureTablesCreatedAsync() { await connection.ExecuteAsync( @@ -57,7 +58,7 @@ CREATE TABLE IF NOT EXISTS Settings( CREATE TABLE IF NOT EXISTS WidgetType( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL); - + CREATE TABLE IF NOT EXISTS Widget( Id INTEGER PRIMARY KEY AUTOINCREMENT, WidgetType INTEGER, @@ -75,8 +76,7 @@ CREATE TABLE IF NOT EXISTS ProviderKeys( CREATE TABLE IF NOT EXISTS WeatherData( Id INTEGER PRIMARY KEY AUTOINCREMENT, - CardId INTEGER NOT NULL, - Address TEXT NOT NULL, + Address TEXT NOT NULL UNIQUE, Timezone TEXT NOT NULL, Description TEXT NOT NULL, Temp REAL NOT NULL, @@ -84,8 +84,17 @@ CREATE TABLE IF NOT EXISTS WeatherData( Humidity REAL NOT NULL, WindSpeed REAL NOT NULL, WindDir REAL NOT NULL, - FetchedAt TEXT NOT NULL, - FOREIGN KEY(CardId) REFERENCES Card(Id)); + FetchedAt TEXT NOT NULL); + + CREATE TABLE IF NOT EXISTS WeatherDataConnection( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + CardId INTEGER NOT NULL, + WeatherId INTEGER NOT NULL, + UNIQUE(CardId), + FOREIGN KEY(CardId) REFERENCES Card(Id) ON DELETE CASCADE, + FOREIGN KEY(WeatherId) REFERENCES WeatherData(Id) ON DELETE CASCADE); + + CREATE INDEX IF NOT EXISTS idx_weatherdataconnection_weatherid ON WeatherDataConnection(WeatherId); INSERT INTO WidgetType(Name) SELECT 'Empty' @@ -101,8 +110,8 @@ INSERT INTO WidgetType(Name) INSERT INTO Widget(WidgetType, Label, Description, Icon) SELECT Id, 'Weather widget', '', 'clouds' - FROM WidgetType - WHERE Name = 'Weather' + FROM WidgetType + WHERE Name = 'Weather' AND NOT EXISTS (SELECT 1 FROM Widget WHERE Id = 1); INSERT INTO Widget(WidgetType, Label, Description, Icon) @@ -117,5 +126,75 @@ FROM WidgetType WHERE Name = 'Custom' AND NOT EXISTS (SELECT 1 FROM Widget WHERE Id = 3);", commandTimeout:150); + + await MigrateLegacyWeatherDataAsync(); + } + + private async Task MigrateLegacyWeatherDataAsync() + { + var columnNames = await connection.QueryAsync( + "SELECT name FROM pragma_table_info('WeatherData');"); + + if (!columnNames.Contains("CardId")) + return; + + BackupDatabaseFile(); + + await connection.ExecuteAsync( + sql: @" + PRAGMA foreign_keys=OFF; + BEGIN TRANSACTION; + + CREATE TABLE WeatherData_new( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Address TEXT NOT NULL UNIQUE, + Timezone TEXT NOT NULL, + Description TEXT NOT NULL, + Temp REAL NOT NULL, + FeelsLike REAL NOT NULL, + Humidity REAL NOT NULL, + WindSpeed REAL NOT NULL, + WindDir REAL NOT NULL, + FetchedAt TEXT NOT NULL); + + INSERT INTO WeatherData_new (Address, Timezone, Description, Temp, FeelsLike, Humidity, WindSpeed, WindDir, FetchedAt) + SELECT Address, Timezone, Description, Temp, FeelsLike, Humidity, WindSpeed, WindDir, FetchedAt + FROM WeatherData wd + WHERE wd.Id = ( + SELECT wd2.Id FROM WeatherData wd2 + WHERE wd2.Address = wd.Address + ORDER BY wd2.FetchedAt DESC, wd2.Id DESC + LIMIT 1 + ); + + INSERT INTO WeatherDataConnection (CardId, WeatherId) + SELECT legacy.CardId, wn.Id + FROM ( + SELECT wd.CardId, wd.Address + FROM WeatherData wd + WHERE wd.Id = ( + SELECT wd2.Id FROM WeatherData wd2 + WHERE wd2.CardId = wd.CardId + ORDER BY wd2.FetchedAt DESC, wd2.Id DESC + LIMIT 1 + ) + ) legacy + INNER JOIN WeatherData_new wn ON wn.Address = legacy.Address; + + DROP TABLE WeatherData; + ALTER TABLE WeatherData_new RENAME TO WeatherData; + + COMMIT; + PRAGMA foreign_keys=ON;", + commandTimeout: 150); + } + + private void BackupDatabaseFile() + { + var dataSource = new SqliteConnectionStringBuilder(connection.ConnectionString).DataSource; + if (string.IsNullOrWhiteSpace(dataSource) || !File.Exists(dataSource)) + return; + + File.Copy(dataSource, dataSource + ".bak", overwrite: true); } } diff --git a/Dtos/WeatherDataConnectionDtoModel.cs b/Dtos/WeatherDataConnectionDtoModel.cs new file mode 100644 index 00000000..c568da39 --- /dev/null +++ b/Dtos/WeatherDataConnectionDtoModel.cs @@ -0,0 +1,8 @@ +namespace Gridly.Dtos; + +public class WeatherDataConnectionDtoModel +{ + public int? Id { get; set; } + public int? CardId { get; set; } + public int? WeatherId { get; set; } +} diff --git a/Factories/WeatherDataConnectionFactory.cs b/Factories/WeatherDataConnectionFactory.cs new file mode 100644 index 00000000..6adf5371 --- /dev/null +++ b/Factories/WeatherDataConnectionFactory.cs @@ -0,0 +1,14 @@ +using Gridly.Dtos; + +namespace Gridly.Factories +{ + public class WeatherDataConnectionFactory + { + public static WeatherDataConnectionDtoModel Create(int cardId, int weatherId) + => new WeatherDataConnectionDtoModel + { + CardId = cardId, + WeatherId = weatherId, + }; + } +} diff --git a/Gridly-Client/src/app/components/card/card.component.html b/Gridly-Client/src/app/components/card/card.component.html index 71de364e..650bc659 100644 --- a/Gridly-Client/src/app/components/card/card.component.html +++ b/Gridly-Client/src/app/components/card/card.component.html @@ -50,7 +50,7 @@ {{card.iconData?.materialIcon}} @if(storedWeatherData$ | async; as weatherData){ @for(weather of weatherData; track weather){ - @if (card.id === weather.cardId && card.type === CardTypes.Weather) { + @if (card.type === CardTypes.Weather) {