Innovenergy_trunk/csharp/App/Backend/Database/Db.cs

439 lines
18 KiB
C#

using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.Lib.S3Utils;
using InnovEnergy.Lib.S3Utils.DataTypes;
using InnovEnergy.Lib.Utils;
using SQLite;
using SQLiteConnection = SQLite.SQLiteConnection;
namespace InnovEnergy.App.Backend.Database;
//The methods of the Db class are located in multiple files (Create.cs, Read,cs, Delete.cs, Update.cs)
//That's why the class definition is partial
public static partial class Db
{
private static SQLiteConnection Connection { get; } = InitConnection();
public static TableQuery<Session> Sessions => Connection.Table<Session>();
public static TableQuery<Folder> Folders => Connection.Table<Folder>();
public static TableQuery<Installation> Installations => Connection.Table<Installation>();
public static TableQuery<User> Users => Connection.Table<User>();
public static TableQuery<FolderAccess> FolderAccess => Connection.Table<FolderAccess>();
public static TableQuery<InstallationAccess> InstallationAccess => Connection.Table<InstallationAccess>();
public static TableQuery<OrderNumber2Installation> OrderNumber2Installation => Connection.Table<OrderNumber2Installation>();
public static TableQuery<Error> Errors => Connection.Table<Error>();
public static TableQuery<Warning> Warnings => Connection.Table<Warning>();
public static TableQuery<UserAction> UserActions => Connection.Table<UserAction>();
public static TableQuery<WeeklyReportSummary> WeeklyReports => Connection.Table<WeeklyReportSummary>();
public static TableQuery<MonthlyReportSummary> MonthlyReports => Connection.Table<MonthlyReportSummary>();
public static TableQuery<YearlyReportSummary> YearlyReports => Connection.Table<YearlyReportSummary>();
public static TableQuery<DailyEnergyRecord> DailyRecords => Connection.Table<DailyEnergyRecord>();
public static TableQuery<HourlyEnergyRecord> HourlyRecords => Connection.Table<HourlyEnergyRecord>();
public static TableQuery<AiInsightCache> AiInsightCaches => Connection.Table<AiInsightCache>();
public static TableQuery<EmailPreference> EmailPreferences => Connection.Table<EmailPreference>();
// Ticket system tables
public static TableQuery<Ticket> Tickets => Connection.Table<Ticket>();
public static TableQuery<TicketComment> TicketComments => Connection.Table<TicketComment>();
public static TableQuery<TicketAiDiagnosis> TicketAiDiagnoses => Connection.Table<TicketAiDiagnosis>();
public static TableQuery<TicketTimelineEvent> TicketTimelineEvents => Connection.Table<TicketTimelineEvent>();
// Document storage
public static TableQuery<Document> Documents => Connection.Table<Document>();
// Checklist
public static TableQuery<ChecklistItem> ChecklistItems => Connection.Table<ChecklistItem>();
public static void Init()
{
//Used to force static constructor
//Since this class is static, we call Init method from the Program.cs to initialize all the fields of the class
//When a class is loaded, the fields are initialized before the constructor's code is executed.
//The TableQuery fields are lazy meaning that they will be initialized when they get accessed
//The connection searches for the latest backup and binds all the tables to it.
}
//This is the constructor of the class
static Db()
{
Connection.RunInTransaction(() =>
{
Connection.CreateTable<User>();
Connection.CreateTable<Installation>();
Connection.CreateTable<Folder>();
Connection.CreateTable<FolderAccess>();
Connection.CreateTable<InstallationAccess>();
Connection.CreateTable<Session>();
Connection.CreateTable<OrderNumber2Installation>();
Connection.CreateTable<Error>();
Connection.CreateTable<Warning>();
Connection.CreateTable<UserAction>();
Connection.CreateTable<WeeklyReportSummary>();
Connection.CreateTable<MonthlyReportSummary>();
Connection.CreateTable<YearlyReportSummary>();
Connection.CreateTable<DailyEnergyRecord>();
Connection.CreateTable<HourlyEnergyRecord>();
Connection.CreateTable<AiInsightCache>();
Connection.CreateTable<EmailPreference>();
// Ticket system tables
Connection.CreateTable<Ticket>();
Connection.CreateTable<TicketComment>();
Connection.CreateTable<TicketAiDiagnosis>();
Connection.CreateTable<TicketTimelineEvent>();
// Document storage
Connection.CreateTable<Document>();
// Checklist
Connection.CreateTable<ChecklistItem>();
});
// One-time migration: normalize legacy long-form language values to ISO codes
Connection.Execute("UPDATE User SET Language = 'en' WHERE Language IS NULL OR Language = '' OR Language = 'english'");
Connection.Execute("UPDATE User SET Language = 'de' WHERE Language = 'german'");
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
// Backfill: SQLite-net adds new bool columns as nullable with NULL for existing rows.
// LINQ `.Where(i => i.DataCollectionEnabled)` translates to `WHERE ... = 1` and excludes
// NULL rows, which would silently disable ingestion for every pre-existing installation.
Connection.Execute("UPDATE Installation SET DataCollectionEnabled = 1 WHERE DataCollectionEnabled IS NULL");
// One-time migration: rebrand to inesco energy
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'InnovEnergy'");
Connection.Execute("UPDATE Folder SET Name = 'inesco energy' WHERE Name = 'inesco Energy'");
Connection.Execute("UPDATE Folder SET Name = 'Sodistore Max Installations' WHERE Name = 'SodistoreMax Installations'");
Connection.Execute("UPDATE User SET Name = 'inesco energy Master Admin' WHERE Name = 'InnovEnergy Master Admin'");
Connection.Execute("UPDATE User SET Name = 'inesco energy Master Admin' WHERE Name = 'inesco Energy Master Admin'");
// One-time migration: rewrite early-seeded English subtask text to translation keys so the
// frontend can localize them. Idempotent: rows already containing keys match nothing.
var subtaskTextToKey = new (String Old, String Key)[]
{
("Customer information (email, address)", "checklistStep5Sub1"),
("Installation information (external EMS, grid provider, data collection)", "checklistStep5Sub2"),
("Installation information (external EMS, network provider, data collection)", "checklistStep5Sub2"),
("Battery serial number", "checklistStep5Sub3"),
("Inverter serial number", "checklistStep5Sub4"),
("Data logger serial number", "checklistStep5Sub5"),
("VPN details", "checklistStep5Sub6"),
("Inverter firmware and configuration verified", "checklistStep6Sub1"),
("Battery firmware and configuration verified", "checklistStep6Sub2"),
("Internet for gateway configured", "checklistStep6Sub3"),
("Communication cable between gateway and inverter correct", "checklistStep6Sub4"),
("S3 bucket number and key credentials copied from Information tab into config.json","checklistStep7Sub1"),
("Product ID configured in config.json", "checklistStep7Sub2"),
("USB ID configured in config.json", "checklistStep7Sub3"),
("Inverter data reading from inverter tested", "checklistStep7Sub4")
};
foreach (var (oldText, key) in subtaskTextToKey)
{
Connection.Execute(
"UPDATE ChecklistItem SET Subtasks = REPLACE(Subtasks, ?, ?) WHERE Subtasks LIKE ?",
$"\"{oldText}\"", $"\"{key}\"", $"%\"{oldText}\"%");
}
// One-time backfill: step 3 originally had no subtasks; add the installation serial subtask
// to existing rows.
Connection.Execute(
"UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 3 AND (Subtasks IS NULL OR Subtasks = '')",
"[{\"text\":\"checklistStep3Sub1\",\"checked\":false}]");
// One-time backfill: step 8 originally had no subtasks; add the delivery-receipt subtask
// to existing rows so already-seeded installations pick up the new subtask after deploy.
Connection.Execute(
"UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 8 AND (Subtasks IS NULL OR Subtasks = '')",
"[{\"text\":\"checklistStep8Sub1\",\"checked\":false}]");
// One-time backfill: step 10 originally had no subtasks; add the two upload subtasks
// (installation protocol + time & material report) to existing rows.
Connection.Execute(
"UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 10 AND (Subtasks IS NULL OR Subtasks = '')",
"[{\"text\":\"checklistStep10Sub1\",\"checked\":false},{\"text\":\"checklistStep10Sub2\",\"checked\":false}]");
//UpdateKeys();
CleanupSessions().SupressAwaitWarning();
DeleteSnapshots().SupressAwaitWarning();
}
private static SQLiteConnection InitConnection()
{
var latestDb = new DirectoryInfo("DbBackups")
.GetFiles()
.OrderBy(f => f.LastWriteTime)
.Last().Name;
Console.WriteLine("latestdb is "+latestDb);
//This is the file connection from the DbBackups folder
var fileConnection = new SQLiteConnection("DbBackups/" + latestDb);
//Create a table if it does not exist
fileConnection.CreateTable<User>();
fileConnection.CreateTable<Installation>();
fileConnection.CreateTable<Folder>();
fileConnection.CreateTable<FolderAccess>();
fileConnection.CreateTable<InstallationAccess>();
fileConnection.CreateTable<Session>();
fileConnection.CreateTable<OrderNumber2Installation>();
fileConnection.CreateTable<Error>();
fileConnection.CreateTable<Warning>();
fileConnection.CreateTable<UserAction>();
fileConnection.CreateTable<WeeklyReportSummary>();
fileConnection.CreateTable<MonthlyReportSummary>();
fileConnection.CreateTable<YearlyReportSummary>();
fileConnection.CreateTable<DailyEnergyRecord>();
fileConnection.CreateTable<HourlyEnergyRecord>();
fileConnection.CreateTable<AiInsightCache>();
fileConnection.CreateTable<EmailPreference>();
// Ticket system tables
fileConnection.CreateTable<Ticket>();
fileConnection.CreateTable<TicketComment>();
fileConnection.CreateTable<TicketCommentMention>();
fileConnection.CreateTable<TicketAiDiagnosis>();
fileConnection.CreateTable<TicketTimelineEvent>();
// Document storage
fileConnection.CreateTable<Document>();
// Checklist
fileConnection.CreateTable<ChecklistItem>();
// Migrate new columns: set defaults for existing rows where NULL or empty
fileConnection.Execute("UPDATE Installation SET ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
fileConnection.Execute("UPDATE Installation SET PvStringsPerInverter = '' WHERE PvStringsPerInverter IS NULL");
return fileConnection;
//return CopyDbToMemory(fileConnection);
}
public static void BackupDatabase()
{
var filename = "db-" + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ".sqlite";
Connection.Backup("DbBackups/" + filename);
}
//Delete all except 10 snapshots every 24 hours.
private static async Task DeleteSnapshots()
{
while (true)
{
try
{
var files = new DirectoryInfo("DbBackups")
.GetFiles()
.OrderByDescending(f => f.LastWriteTime);
var filesToDelete = files.Skip(10);
foreach (var file in filesToDelete)
{
Console.WriteLine("File to delete is " + file.Name);
file.Delete();
}
}
catch(Exception e)
{
Console.WriteLine("An error has occured when cleaning database snapshots, exception is:\n"+e);
}
await Task.Delay(TimeSpan.FromHours(24));
}
}
//Delete all expired sessions every half an hour. An expired session is a session remained for more than 1 day.
private static async Task CleanupSessions()
{
while (true)
{
try
{
var deadline = DateTime.Now.AddDays(-Session.MaxAge.Days);
foreach (var session in Sessions)
{
if (session.LastSeen < deadline)
{
Console.WriteLine("Need to remove session of user id " + session.User.Name + "last time is "+session.LastSeen);
}
}
Sessions.Delete(s => s.LastSeen < deadline);
}
catch(Exception e)
{
Console.WriteLine("An error has occured when cleaning stale sessions, exception is:\n"+e);
}
await Task.Delay(TimeSpan.FromHours(0.5));
}
}
private static async Task RemoveNonExistingKeys()
{
while (true)
{
try
{
var validReadKeys = Installations
.Select(i => i.S3Key)
.Distinct()
.ToList();
var validWriteKeys = Installations
.Select(i => i.S3WriteKey)
.Distinct()
.ToList();
Console.WriteLine("VALID READ KEYS");
for (int i = 0; i < validReadKeys.Count; i++)
{
Console.WriteLine(validReadKeys[i]);
}
Console.WriteLine("VALID WRITE KEYS");
for (int i = 0; i < validReadKeys.Count; i++)
{
Console.WriteLine(validWriteKeys[i]);
}
const String provider = "exo.io";
var S3keys = await ExoCmd.GetAccessKeys();
foreach (var keyMetadata in S3keys)
{
if (keyMetadata["key"].ToString()!="EXOa0b53cf10517307cec1bf00e" && !validReadKeys.Contains(keyMetadata["key"].ToString()) && !validWriteKeys.Contains(keyMetadata["key"].ToString()))
{
//await ExoCmd.RevokeReadKey(keyMetadata["key"].ToString());
Console.WriteLine("Deleted key "+keyMetadata["key"]);
}
}
}
catch(Exception e)
{
Console.WriteLine("An error has occured when updating S3 keys, exception is:\n"+e);
}
await Task.Delay(TimeSpan.FromHours(24));
}
}
private static async Task UpdateKeys()
{
while (true)
{
try
{
await UpdateS3Urls();
}
catch(Exception e)
{
Console.WriteLine("An error has occured when updating S3 keys, exception is:\n"+e);
}
await RemoveNonExistingKeys();
await Task.Delay(TimeSpan.FromHours(24));
}
}
private static Boolean RunTransaction(Func<Boolean> func)
{
var savepoint = Connection.SaveTransactionPoint();
var success = false;
try
{
success = func();
}
finally
{
if (success)
Connection.Release(savepoint);
else
Connection.RollbackTo(savepoint);
}
return success;
}
private static async Task UpdateS3Urls()
{
var regions = Installations
.Select(i => i.S3Region)
.Distinct()
.ToList();
const String provider = "exo.io";
Console.WriteLine("-----------------------UPDATED READ KEYS-------------------------------------------------------------------");
foreach (var region in regions)
{
var s3Region = new S3Region($"https://{region}.{provider}", ExoCmd.S3Credentials!);
var bucketList = await s3Region.ListAllBuckets();
var installations = from bucket in bucketList.Buckets
from installation in Installations
where installation.BucketName() == bucket.BucketName
select installation;
foreach (var installation in installations)
{
await installation.RenewS3Credentials();
}
}
}
public static async Task<Boolean> SendPasswordResetEmail(User user, String sessionToken)
{
try
{
await user.SendPasswordResetEmail(sessionToken);
return true;
}
catch
{
Console.WriteLine("return false");
return false;
}
}
public static async Task<Boolean> SendNewUserEmail(User user)
{
try
{
await user.SendNewUserWelcomeMessage();
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Welcome email failed for {user.Email}");
Console.WriteLine(ex.ToString());
return false;
}
}
public static Boolean DeleteUserPassword(User user)
{
user.Password = "";
user.MustResetPassword = true;
return Update(user);
}
}