1052 lines
45 KiB
C#
1052 lines
45 KiB
C#
using System;
|
|
using Microsoft.Extensions.Logging;
|
|
using AyaNova.Models;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Linq;
|
|
|
|
namespace AyaNova.Util
|
|
{
|
|
|
|
|
|
internal static class DbUtil
|
|
{
|
|
private static string _RawAyaNovaConnectionString;
|
|
private static string _dbConnectionString;
|
|
private static string _dbName;
|
|
private static string _dbUserName;
|
|
private static string _dbPassword;
|
|
private static string _dbServer;
|
|
|
|
#region parse connection string
|
|
internal static void ParseConnectionString(ILogger _log, string AyaNovaConnectionString)
|
|
{
|
|
|
|
if (string.IsNullOrWhiteSpace(AyaNovaConnectionString))
|
|
{
|
|
_log.LogDebug("There is no database server connection string set, AYANOVA_DB_CONNECTION is missing or empty. Will use default: \"Server=localhost;Username=postgres;Database=AyaNova;\"");
|
|
AyaNovaConnectionString = "Server=localhost;Username=postgres;Database=AyaNova;";
|
|
}
|
|
|
|
_RawAyaNovaConnectionString = AyaNovaConnectionString;
|
|
var builder = new System.Data.Common.DbConnectionStringBuilder();
|
|
builder.ConnectionString = AyaNovaConnectionString;
|
|
|
|
if (!builder.ContainsKey("database"))
|
|
{
|
|
_log.LogDebug("There is no database name specified (\"Database=<NAME>\") in connection string. Will use default: \"Database=AyaNova;\"");
|
|
builder.Add("database", "AyaNova");
|
|
}
|
|
|
|
//Keep track of default values
|
|
_dbConnectionString = builder.ConnectionString;
|
|
if (builder.ContainsKey("database"))
|
|
_dbName = builder["database"].ToString();
|
|
|
|
if (builder.ContainsKey("username"))
|
|
_dbUserName = builder["username"].ToString();
|
|
|
|
if (builder.ContainsKey("password"))
|
|
_dbPassword = builder["password"].ToString();
|
|
|
|
if (builder.ContainsKey("server"))
|
|
_dbServer = builder["server"].ToString();
|
|
|
|
_log.LogDebug("AyaNova will use the following connection string: {0}", PasswordRedactedConnectionString(_dbConnectionString));
|
|
|
|
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
//clean out password from connection string
|
|
//for log purposes
|
|
internal static string PasswordRedactedConnectionString(string cs)
|
|
{
|
|
var nStart = 0;
|
|
var nStop = 0;
|
|
var lwrcs = cs.ToLowerInvariant();
|
|
nStart = lwrcs.IndexOf("password");
|
|
if (nStart == -1)
|
|
{
|
|
//no password, just return it
|
|
return cs;
|
|
}
|
|
//find terminating semicolon
|
|
nStop = lwrcs.IndexOf(";", nStart);
|
|
if (nStop == -1 || nStop == lwrcs.Length)
|
|
{
|
|
//no terminating semicolon or that is the final character in the string
|
|
return cs.Substring(0, nStart + 9) + "[redacted];";
|
|
}
|
|
else
|
|
{
|
|
//not the last thing in the string so return the whole string minus the password part
|
|
return cs.Substring(0, nStart + 9) + "[redacted];" + cs.Substring(nStop + 1);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Connection utilities
|
|
///////////////////////////////////////////
|
|
//Verify that server exists
|
|
//
|
|
private static string AdminConnectionString
|
|
{
|
|
get
|
|
{
|
|
return _dbConnectionString.Replace(_dbName, "postgres");
|
|
}
|
|
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
//Connection string without password
|
|
//
|
|
internal static string DisplayableConnectionString
|
|
{
|
|
get
|
|
{
|
|
return PasswordRedactedConnectionString(_dbConnectionString);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
#region DB verification
|
|
|
|
///////////////////////////////////////////
|
|
// Get database server version
|
|
//
|
|
internal static string DBServerVersion(AyaNova.Models.AyContext ct)
|
|
{
|
|
using (var cmd = ct.Database.GetDbConnection().CreateCommand())
|
|
{
|
|
ct.Database.OpenConnection();
|
|
cmd.CommandText = $"select version();";
|
|
using (var dr = cmd.ExecuteReader())
|
|
{
|
|
if (dr.Read())
|
|
{
|
|
if (dr.IsDBNull(0))
|
|
return "Unknown / no results";
|
|
else
|
|
return (dr.GetString(0));
|
|
}
|
|
else
|
|
{
|
|
return "Unknown / no results";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
// Get database runtime parameter settings
|
|
//
|
|
internal static Dictionary<string, string> DBServerRunTimeParameters(AyaNova.Models.AyContext ct)
|
|
{
|
|
Dictionary<string, string> ret = new Dictionary<string, string>();
|
|
using (var cmd = ct.Database.GetDbConnection().CreateCommand())
|
|
{
|
|
ct.Database.OpenConnection();
|
|
cmd.CommandText = $"SHOW ALL;";
|
|
using (var dr = cmd.ExecuteReader())
|
|
{
|
|
while (dr.Read())
|
|
{
|
|
string name = string.Empty; ;
|
|
string setting = string.Empty;
|
|
if (!dr.IsDBNull(0))
|
|
name = dr.GetString(0);
|
|
if (!dr.IsDBNull(1))
|
|
setting = dr.GetString(1);
|
|
ret.Add(name, setting);
|
|
}
|
|
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
//Verify that server exists
|
|
// spend up to 5 minutes waiting for it to come up before bailing
|
|
//
|
|
internal static bool DatabaseServerExists(ILogger log, string logPrepend)
|
|
{
|
|
|
|
try
|
|
{
|
|
//Try every 5 seconds for 60 tries before giving up (5 minutes total)
|
|
|
|
var maxRetryAttempts = 60;
|
|
var pauseBetweenFailures = TimeSpan.FromSeconds(5);
|
|
RetryHelper.RetryOnException(maxRetryAttempts, pauseBetweenFailures, log, logPrepend + DisplayableConnectionString, () =>
|
|
{
|
|
using (var conn = new Npgsql.NpgsqlConnection(AdminConnectionString))
|
|
{
|
|
|
|
conn.Open();
|
|
conn.Close();
|
|
}
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////
|
|
// Set global flag if db server is connectable
|
|
//
|
|
internal static void CheckDatabaseServerAvailable(ILogger log)
|
|
{
|
|
//Called by generator when db is down to check if it can connect
|
|
if (CHECKING_DB_AVAILABLE) return;
|
|
//don't check too often just fills log files for no reason
|
|
if (DateTime.UtcNow - CHECKED_DB_AVAILABLE_LAST < CHECK_DB_AVAILABLE_EVERY_INTERVAL)
|
|
return;
|
|
CHECKING_DB_AVAILABLE = true;
|
|
if (CHECKING_DB_LOG_DOWN_STATUS)
|
|
{
|
|
log.LogInformation("Database server unreachable; pausing Job processing");
|
|
CHECKING_DB_LOG_DOWN_STATUS = false;
|
|
}
|
|
try
|
|
{
|
|
log.LogTrace("Database Down - checking if up yet");
|
|
using (AyContext ct = ServiceProviderProvider.DBContext)
|
|
{
|
|
var dummy = ct.GlobalBizSettings.FirstOrDefault(z => z.Id == 1);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
finally
|
|
{
|
|
CHECKED_DB_AVAILABLE_LAST = DateTime.UtcNow;
|
|
CHECKING_DB_AVAILABLE = false;
|
|
}
|
|
//We have db available
|
|
log.LogInformation("Database server has become available; resuming Job processing");
|
|
ServerGlobalOpsSettingsCache.DBAVAILABLE = true;
|
|
CHECKING_DB_LOG_DOWN_STATUS = true;
|
|
}
|
|
private static bool CHECKING_DB_AVAILABLE = false;
|
|
private static bool CHECKING_DB_LOG_DOWN_STATUS = true;
|
|
private static DateTime CHECKED_DB_AVAILABLE_LAST = DateTime.MinValue;
|
|
private static TimeSpan CHECK_DB_AVAILABLE_EVERY_INTERVAL = new TimeSpan(0, 0, 30);
|
|
|
|
|
|
///////////////////////////////////////////////////////////
|
|
// Check if exception means db server is unavailable
|
|
// if so, flag global flag indicating it isn't
|
|
//
|
|
internal static void HandleIfDatabaseUnavailableTypeException(Exception ex)
|
|
{
|
|
if (ex == null) return;
|
|
if (ex.Message.Contains("transient failure") && ex.Source.Contains("PostgreSQL"))
|
|
ServerGlobalOpsSettingsCache.DBAVAILABLE = false;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
//Verify that database exists, if not, then create it
|
|
//
|
|
internal static bool EnsureDatabaseExists(ILogger _log)
|
|
{
|
|
_log.LogDebug("Ensuring database exists. Connection string is: \"{0}\"", DisplayableConnectionString);
|
|
|
|
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
|
|
{
|
|
try
|
|
{
|
|
conn.Open();
|
|
conn.Close();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
|
|
//if it's a db doesn't exist that's ok, we'll create it, not an error
|
|
if (e is Npgsql.PostgresException)
|
|
{
|
|
if (((Npgsql.PostgresException)e).SqlState == "3D000")
|
|
{
|
|
//create the db here
|
|
using (var cnCreate = new Npgsql.NpgsqlConnection(AdminConnectionString))
|
|
{
|
|
cnCreate.Open();
|
|
|
|
// Create the database desired
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = cnCreate;
|
|
cmd.CommandText = "CREATE DATABASE \"" + _dbName + "\" WITH ENCODING \"UTF8\" TEMPLATE=template0;";
|
|
cmd.ExecuteNonQuery();
|
|
_log.LogInformation("Database \"{0}\" created successfully!", _dbName);
|
|
}
|
|
cnCreate.Close();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var err = string.Format("Database server connection failed. Connection string is: \"{0}\"", DisplayableConnectionString);
|
|
_log.LogCritical(e, "BOOT: E1000 - " + err);
|
|
err = err + "\nError reported was: " + e.Message;
|
|
throw new ApplicationException(err);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region DB utilities
|
|
///////////////////////////////////////////
|
|
// Drop and re-create db
|
|
// This is the NUCLEAR option and
|
|
// completely ditches the DB and all user uploaded files
|
|
//
|
|
internal static async Task DropAndRecreateDbAsync(ILogger _log)
|
|
{
|
|
_log.LogInformation("Dropping and creating Database \"{0}\"", _dbName);
|
|
|
|
//clear all connections so that the database can be dropped
|
|
Npgsql.NpgsqlConnection.ClearAllPools();
|
|
|
|
using (var conn = new Npgsql.NpgsqlConnection(AdminConnectionString))
|
|
{
|
|
await conn.OpenAsync();
|
|
|
|
// Create the database desired
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "DROP DATABASE \"" + _dbName + "\";";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "CREATE DATABASE \"" + _dbName + "\" WITH ENCODING \"UTF8\" TEMPLATE=template0;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
_log.LogDebug("Database created");
|
|
}
|
|
await conn.CloseAsync();
|
|
}
|
|
|
|
//final cleanup step is to erase user uploaded files
|
|
FileUtil.EraseEntireContentsOfAttachmentFilesFolder();
|
|
}
|
|
|
|
|
|
//case 4170
|
|
///////////////////////////////////////////
|
|
// Remove license key from database
|
|
// triggered by AYANOVA_REMOVE_LICENSE_FROM_DB flag on boot
|
|
// This is for potential edge cases when we need
|
|
// to fix a weird license issue for a customer remotely
|
|
// by issuing a new license when they can't boot due to a bad license
|
|
// e.g. they upgrade and no maint so get too new error on boot
|
|
// want to renew and keep working so they contact us, buy a maintenance
|
|
// and need to have it fetch. (or some other unknown future issue)
|
|
//
|
|
internal static void RemoveLicenseFromDB(ILogger _log)
|
|
{
|
|
_log.LogInformation("Removing license from database \"{0}\"", _dbName);
|
|
|
|
//clear all connections so that the database can be dropped
|
|
Npgsql.NpgsqlConnection.ClearAllPools();
|
|
|
|
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
|
|
{
|
|
conn.Open();
|
|
|
|
// Create the database desired
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "delete from alicense;";
|
|
cmd.ExecuteNonQuery();
|
|
_log.LogDebug("License removed");
|
|
}
|
|
conn.Close();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////
|
|
// Erase all user entered data from the db
|
|
// This is called by seeder for trial seeding purposes
|
|
// and by v8 migrate and by license controller when erasing db
|
|
internal static async Task EmptyBizDataFromDatabaseForSeedingOrImportingAsync(ILogger _log, bool keepTaxCodes = false)
|
|
{
|
|
//case 4221
|
|
//erase plan to use truncate table instead of slower delete method
|
|
//- truncate table cascade deletes which removes all user accounts including superuser
|
|
//- duplicate the auser and auseroption tables to a temporary table to preserve them
|
|
//- do the full truncate
|
|
//- select the superuser account back to the auser and useroptions
|
|
//- discover where else this needs to be done
|
|
//- $profit
|
|
_log.LogInformation("Erasing Database \"{0}\"", _dbName);
|
|
AyaNova.Api.ControllerHelpers.ApiServerState apiServerState = (AyaNova.Api.ControllerHelpers.ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(AyaNova.Api.ControllerHelpers.ApiServerState));
|
|
|
|
apiServerState.SetClosed("Erasing database");
|
|
|
|
try
|
|
{
|
|
|
|
//clear all connections so that the database can be dropped
|
|
Npgsql.NpgsqlConnection.ClearAllPools();
|
|
|
|
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
|
|
{
|
|
await conn.OpenAsync();
|
|
|
|
//prepare to delete by removing foreign keys
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "update auser set customerid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "update aunit set replacedbyunitid=null, parentunitid=null, purchasedfromvendorid=null, unitmodelid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "update auser set headofficeid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "update auser set vendorid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "update aloanunit set unitid=null, workorderitemloanid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "update aworkorder set fromquoteid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "update aworkorder set frompmid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "update apurchaseorderitem set workorderitempartrequestid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
|
|
cmd.CommandText = "update aworkorderitem set fromcsrid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
// if (jobIdToKeep != Guid.Empty)
|
|
// {
|
|
// //delete from aopsjoblog where jobid <> '8acec231-aab4-4fdc-b01e-8908378a0e7f';
|
|
// //delete from aopsjob where gid <> '8acec231-aab4-4fdc-b01e-8908378a0e7f';
|
|
// cmd.CommandText = $"delete from aopsjoblog where jobid <> '{jobIdToKeep}'";
|
|
// await cmd.ExecuteNonQueryAsync();
|
|
// cmd.CommandText = $"delete from aopsjob where gid <> '{jobIdToKeep}'";
|
|
// await cmd.ExecuteNonQueryAsync();
|
|
// }
|
|
|
|
|
|
if (!keepTaxCodes)
|
|
{
|
|
cmd.CommandText = "update aglobalbizsettings set taxpartpurchaseid=null,taxpartsaleid=null,taxratesaleid=null;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
//DELIBERATELY NOT ERASED:
|
|
//NOTE: these tables commented below are deliberately not affected as it's assumed for a **COMPLETE** erase users will erase manually or use the config setting to completely drop the database
|
|
//For anything less than that it's assumed users are testing things out and want to keep these tables but erase the biz data
|
|
//aglobalopsbackupsettings
|
|
//aglobalopsnotificationsettings
|
|
//aglobalbizsettings
|
|
//alogo
|
|
//areport
|
|
|
|
//TAX CODES are an edge case where they are generated by the seeder potentially so need to be erased in that case or migrate but might be kept when manually erasing from UI.
|
|
|
|
|
|
//await EraseTableAsync("aopsjoblog", conn, true);
|
|
// await EraseTableAsync("aopsjob", conn, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
//Delete non stock translations
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
|
|
//set to default translation so can delete all non default ones
|
|
cmd.CommandText = "update auseroptions set translationid=1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
//case 4396
|
|
cmd.CommandText = "update acustomernotifysubscription set translationid=1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "delete from atranslationitem where translationid > 4;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "delete from atranslation where id > 4;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
//case 4221 truncate support
|
|
//BACKUP USER AND DATA TO BE PRESERVED THAT TRUNCATE WILL CASCADE DELETE
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "CREATE TABLE auser_backup AS TABLE auser;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.CommandText = "CREATE TABLE auseroptions_backup AS TABLE auseroptions;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
//REMOVE ALL REMAINING DATA
|
|
|
|
await TruncateTableAsync("aunitmeterreading", conn);
|
|
await TruncateTableAsync("acustomerservicerequest", conn);
|
|
|
|
//--- WorkOrder
|
|
await TruncateTableAsync("aworkorderitemexpense", conn);
|
|
await TruncateTableAsync("aworkorderitemlabor", conn);
|
|
await TruncateTableAsync("aworkorderitemloan", conn);
|
|
await TruncateTableAsync("aworkorderitempart", conn);
|
|
await TruncateTableAsync("aworkorderitempartrequest", conn);
|
|
await TruncateTableAsync("aworkorderitemscheduleduser", conn);
|
|
await TruncateTableAsync("aworkorderitemtask", conn);
|
|
await TruncateTableAsync("aworkorderitemtravel", conn);
|
|
await TruncateTableAsync("aworkorderitemunit", conn);
|
|
await TruncateTableAsync("aworkorderitemoutsideservice", conn);
|
|
await TruncateTableAsync("aworkorderitem", conn);
|
|
await TruncateTableAsync("aworkorderstate", conn);
|
|
await TruncateTableAsync("aworkorder", conn);
|
|
//---
|
|
|
|
//--- QUOTE
|
|
await TruncateTableAsync("aquoteitemexpense", conn);
|
|
await TruncateTableAsync("aquoteitemlabor", conn);
|
|
await TruncateTableAsync("aquoteitemloan", conn);
|
|
await TruncateTableAsync("aquoteitempart", conn);
|
|
await TruncateTableAsync("aquoteitemscheduleduser", conn);
|
|
await TruncateTableAsync("aquoteitemtask", conn);
|
|
await TruncateTableAsync("aquoteitemtravel", conn);
|
|
await TruncateTableAsync("aquoteitemunit", conn);
|
|
await TruncateTableAsync("aquoteitemoutsideservice", conn);
|
|
await TruncateTableAsync("aquoteitem", conn);
|
|
await TruncateTableAsync("aquotestate", conn);
|
|
await TruncateTableAsync("aquote", conn);
|
|
//---
|
|
|
|
|
|
//--- PM
|
|
await TruncateTableAsync("apmitemexpense", conn);
|
|
await TruncateTableAsync("apmitemlabor", conn);
|
|
await TruncateTableAsync("apmitemloan", conn);
|
|
await TruncateTableAsync("apmitempart", conn);
|
|
await TruncateTableAsync("apmitemscheduleduser", conn);
|
|
await TruncateTableAsync("apmitemtask", conn);
|
|
await TruncateTableAsync("apmitemtravel", conn);
|
|
await TruncateTableAsync("apmitemunit", conn);
|
|
await TruncateTableAsync("apmitemoutsideservice", conn);
|
|
await TruncateTableAsync("apmitem", conn);
|
|
await TruncateTableAsync("apm", conn);//bugbug
|
|
//---
|
|
|
|
await TruncateTableAsync("afileattachment", conn);
|
|
await TruncateTableAsync("aevent", conn);
|
|
await TruncateTableAsync("adatalistsavedfilter", conn);
|
|
await TruncateTableAsync("adatalistcolumnview", conn);
|
|
await TruncateTableAsync("apicklisttemplate", conn, true);
|
|
await TruncateTableAsync("aformcustom", conn);
|
|
await TruncateTableAsync("asearchkey", conn);
|
|
await TruncateTableAsync("asearchdictionary", conn);
|
|
await TruncateTableAsync("atag", conn);
|
|
await TruncateTableAsync("apurchaseorderitem", conn);
|
|
await TruncateTableAsync("apurchaseorder", conn);
|
|
|
|
await TruncateTableAsync("apartassemblyitem", conn);
|
|
await TruncateTableAsync("apartassembly", conn);
|
|
await TruncateTableAsync("apartinventory", conn);
|
|
await TruncateTableAsync("apart", conn);
|
|
|
|
await TruncateTableAsync("aloanunit", conn);
|
|
await TruncateTableAsync("aunitmodel", conn);
|
|
await TruncateTableAsync("avendor", conn);
|
|
|
|
await TruncateTableAsync("aunit", conn);
|
|
await TruncateTableAsync("aproject", conn);//depends on User, dependants are wo,quote,pm
|
|
|
|
await TruncateTableAsync("acustomernote", conn);
|
|
await TruncateTableAsync("acustomer", conn);
|
|
await TruncateTableAsync("aheadoffice", conn);
|
|
await TruncateTableAsync("acontract", conn);
|
|
|
|
//----- NOTIFICATION
|
|
await TruncateTableAsync("ainappnotification", conn);
|
|
await TruncateTableAsync("anotifyevent", conn);
|
|
await TruncateTableAsync("anotifydeliverylog", conn);
|
|
await TruncateTableAsync("anotifysubscription", conn);
|
|
await TruncateTableAsync("acustomernotifyevent", conn);
|
|
await TruncateTableAsync("acustomernotifydeliverylog", conn);
|
|
await TruncateTableAsync("acustomernotifysubscription", conn);
|
|
|
|
await TruncateTableAsync("amemo", conn);
|
|
await TruncateTableAsync("areminder", conn);//depends on User
|
|
await TruncateTableAsync("areview", conn);//depends on User
|
|
|
|
|
|
await TruncateTableAsync("aservicerate", conn);
|
|
await TruncateTableAsync("atravelrate", conn);
|
|
if (!keepTaxCodes)
|
|
await EraseTableAsync("ataxcode", conn);
|
|
|
|
|
|
await TruncateTableAsync("aquotestatus", conn);
|
|
await TruncateTableAsync("aworkorderstatus", conn);
|
|
await TruncateTableAsync("aworkorderitemstatus", conn);
|
|
await TruncateTableAsync("aworkorderitempriority", conn);
|
|
await TruncateTableAsync("ataskgroup", conn);//items cascade
|
|
|
|
await TruncateTableAsync("ametricmm", conn, true);
|
|
await TruncateTableAsync("ametricdd", conn, true);
|
|
await TruncateTableAsync("adashboardview", conn);
|
|
|
|
await TruncateTableAsync("aintegration", conn);
|
|
|
|
//############# WARNING: there can be unintended consequences easily if new tables or fields are added that REFERENCE other tables triggering a cascade delete unexpectedly
|
|
//be sure about that before making changes and test thoroughly anything that calls this method:
|
|
// the seeding and manual erase and v8-migrate code when making such changes
|
|
|
|
|
|
//case 4221 truncate support
|
|
//COPY BACK USER AND DATA TO BE PRESERVED THAT TRUNCATE WILL CASCADE DELETE
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
//AT this point the truncate commands in erasetable above have caused all user and useroptions to be deleted
|
|
//so no need to clean out those tables, instead put our backup superuser back in again
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "INSERT INTO auser (active, name, lastlogin, login, password, salt , roles, currentauthtoken, "
|
|
+ "dlkey, dlkeyexpire, totpsecret, temptoken, twofactorenabled, passwordresetcode, passwordresetcodeexpire, usertype, "
|
|
+ "employeenumber, notes, wiki, customfields, tags, allowlogin) "
|
|
+ "SELECT active, name, lastlogin, login, password, salt , roles, currentauthtoken, "
|
|
+ "dlkey, dlkeyexpire, totpsecret, temptoken, twofactorenabled, passwordresetcode, passwordresetcodeexpire, usertype, "
|
|
+ "employeenumber, notes, wiki, customfields, tags, allowlogin "
|
|
+ "FROM auser_backup where id = 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "INSERT INTO auseroptions (userid, translationid, languageoverride, timezoneoverride, "
|
|
+ "currencyname, hour12, emailaddress, phone1, phone2, phone3, mapurltemplate, uicolor) "
|
|
+ "SELECT userid, translationid, languageoverride, timezoneoverride, "
|
|
+ "currencyname, hour12, emailaddress, phone1, phone2, phone3, mapurltemplate, uicolor "
|
|
+ "FROM auseroptions_backup where userid = 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
cmd.CommandText = "DROP TABLE IF EXISTS AUSEROPTIONS_BACKUP, AUSER_BACKUP;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
|
|
//final housekeeping
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
|
|
cmd.CommandText = "delete from \"apartwarehouse\" where id <> 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.CommandText = $"ALTER SEQUENCE apartwarehouse_id_seq RESTART WITH 2;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.CommandText = "ALTER SEQUENCE apurchaseorder_serial_seq RESTART WITH 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.CommandText = "ALTER SEQUENCE aworkorder_serial_seq RESTART WITH 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.CommandText = "ALTER SEQUENCE aquote_serial_seq RESTART WITH 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
cmd.CommandText = "ALTER SEQUENCE apm_serial_seq RESTART WITH 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
}
|
|
|
|
await conn.CloseAsync();
|
|
}
|
|
|
|
//If we got here then it's safe to erase the attachment files
|
|
FileUtil.EraseEntireContentsOfAttachmentFilesFolder();
|
|
|
|
_log.LogInformation("Importing any missing stock Report templates");
|
|
await AyaNova.Biz.PrimeData.PrimeReportTemplates();
|
|
apiServerState.ResumePriorState();
|
|
_log.LogInformation("Database erase completed");
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//NOTE: deliberately leaving server in CLOSED state so users see that there is a problem
|
|
//and investigate
|
|
_log.LogCritical(ex, "Unable to erase database due to unexpected condition. Please contact technical support to resolve.");
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
// Truncate all data from the table specified
|
|
//
|
|
private static async Task TruncateTableAsync(string sTable, Npgsql.NpgsqlConnection conn, bool tableHasNoSequence = false)
|
|
{
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = "TRUNCATE \"" + sTable + "\" RESTART IDENTITY CASCADE;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
// Erase all data from the table specified
|
|
//
|
|
private static async Task EraseTableAsync(string sTable, Npgsql.NpgsqlConnection conn, bool tableHasNoSequence = false)
|
|
{
|
|
//this variant is used for tables that don't need to be truncated due to smaller size
|
|
//and would trigger need to backup referenced tables first
|
|
using (var cmd = new Npgsql.NpgsqlCommand())
|
|
{
|
|
cmd.Connection = conn;
|
|
cmd.CommandText = $"delete from {sTable};";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
if (!tableHasNoSequence)
|
|
{
|
|
cmd.CommandText = $"ALTER SEQUENCE {sTable}_id_seq RESTART WITH 1;";
|
|
await cmd.ExecuteNonQueryAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
// Check if DB is empty
|
|
// CALLED BY LICENSE CONTROLLER AND LICENSE.CS FOR TRIAL Request check
|
|
internal static async Task<bool> DBIsEmptyAsync(AyContext ct, ILogger _log)
|
|
{
|
|
//For efficiency just check a few main tables just stuff that would be shitty to have to re-enter
|
|
//Mostly user, customer and vendor cover it because nearly everything else requires those to have any sort of data at all
|
|
|
|
_log.LogDebug("DB empty check");
|
|
|
|
//An empty db contains only one User
|
|
if (await ct.User.LongCountAsync() > 1) return false;
|
|
if (await ct.Customer.AnyAsync()) return false;
|
|
if (await ct.Vendor.AnyAsync()) return false;
|
|
if (await ct.WorkOrder.AnyAsync()) return false;
|
|
if (await ct.Quote.AnyAsync()) return false;
|
|
if (await ct.PM.AnyAsync()) return false;
|
|
if (await ct.Unit.AnyAsync()) return false;
|
|
if (await ct.HeadOffice.AnyAsync()) return false;
|
|
if (await ct.LoanUnit.AnyAsync()) return false;
|
|
if (await ct.Part.AnyAsync()) return false;
|
|
if (await ct.Project.AnyAsync()) return false;
|
|
if (await ct.PurchaseOrder.AnyAsync()) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////
|
|
// Check if DB has evaluation user accounts
|
|
// CALLED BY by login ping from client via notify controller
|
|
internal static async Task<bool> DBHasTrialUsersAsync(AyContext ct, ILogger _log)
|
|
{
|
|
_log.LogDebug("DB trial users presence check for pre-login ping");
|
|
//There are 22 trial users (more but for internal use) in a trial database
|
|
if (await ct.User.LongCountAsync() < 22) return false;
|
|
|
|
//just check for a few for testing
|
|
if (await ct.User.AsNoTracking()
|
|
.Where(z =>
|
|
z.Login == "BizAdmin" ||
|
|
z.Login == "Service" ||
|
|
z.Login == "Inventory" ||
|
|
z.Login == "Accounting" ||
|
|
z.Login == "Tech"
|
|
).LongCountAsync() < 5) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
// Ensure the db is not modified
|
|
//
|
|
internal static async Task CheckFingerPrintAsync(
|
|
long ExpectedColumns,
|
|
long ExpectedIndexes,
|
|
long ExpectedCheckConstraints,
|
|
long ExpectedForeignKeyConstraints,
|
|
long ExpectedViews,
|
|
long ExpectedRoutines,
|
|
ILogger _log)
|
|
{
|
|
_log.LogDebug("Checking DB integrity");
|
|
|
|
long actualColumns = 0;
|
|
long actualIndexes = 0;
|
|
long actualCheckConstraints = 0;
|
|
long actualForeignKeyConstraints = 0;
|
|
long actualViews = 0;
|
|
long actualRoutines = 0;
|
|
|
|
//COLUMNS
|
|
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
|
|
{
|
|
await conn.OpenAsync();
|
|
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
//Count all columns in all our tables
|
|
command.CommandText = "SELECT count(*) FROM information_schema.columns where table_schema='public'";
|
|
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
//check the values
|
|
await result.ReadAsync();
|
|
actualColumns = result.GetInt64(0);
|
|
}
|
|
else
|
|
{
|
|
var err = "E1030 - Database integrity check failed, could not obtain COLUMN data. Contact support.";
|
|
_log.LogCritical(err);
|
|
|
|
throw new ApplicationException(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
//INDEXES
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
//Count all indexes in all our tables
|
|
command.CommandText = "select Count(*) from pg_indexes where schemaname='public'";
|
|
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
//check the values
|
|
await result.ReadAsync();
|
|
actualIndexes = result.GetInt64(0);
|
|
}
|
|
else
|
|
{
|
|
var err = "E1030 - Database integrity check failed, could not obtain INDEX data. Contact support.";
|
|
_log.LogCritical(err);
|
|
throw new ApplicationException(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
//CHECK CONSTRAINTS
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
|
|
command.CommandText = "SELECT count(*) FROM information_schema.check_constraints where constraint_schema='public'";
|
|
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
//check the values
|
|
await result.ReadAsync();
|
|
actualCheckConstraints = result.GetInt64(0);
|
|
}
|
|
else
|
|
{
|
|
var err = "E1030 - Database integrity check failed, could not obtain CHECK CONSTRAINT data. Contact support.";
|
|
_log.LogCritical(err);
|
|
throw new ApplicationException(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
//FOREIGN KEY CONSTRAINTS
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
|
|
command.CommandText = "SELECT count(*) FROM information_schema.referential_constraints where constraint_schema='public'";
|
|
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
//check the values
|
|
await result.ReadAsync();
|
|
actualForeignKeyConstraints = result.GetInt64(0);
|
|
}
|
|
else
|
|
{
|
|
var err = "E1030 - Database integrity check failed, could not obtain FOREIGN KEY CONSTRAINT data. Contact support.";
|
|
_log.LogCritical(err);
|
|
throw new ApplicationException(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
//VIEWS
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
|
|
command.CommandText = "SELECT count(*) FROM information_schema.views where table_schema='public'";
|
|
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
//check the values
|
|
await result.ReadAsync();
|
|
actualViews = result.GetInt64(0);
|
|
}
|
|
else
|
|
{
|
|
var err = "E1030 - Database integrity check failed, could not obtain VIEW data. Contact support.";
|
|
_log.LogCritical(err);
|
|
throw new ApplicationException(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
//ROUTINES
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
|
|
command.CommandText = "SELECT count(*) FROM information_schema.routines where routine_schema='public'";
|
|
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
//check the values
|
|
await result.ReadAsync();
|
|
actualRoutines = result.GetInt64(0);
|
|
}
|
|
else
|
|
{
|
|
var err = "E1030 - Database integrity check failed, could not obtain ROUTINE data. Contact support.";
|
|
_log.LogCritical(err);
|
|
throw new ApplicationException(err);
|
|
}
|
|
}
|
|
}
|
|
await conn.CloseAsync();
|
|
|
|
|
|
|
|
if (ExpectedColumns != actualColumns
|
|
|| ExpectedIndexes != actualIndexes
|
|
|| ExpectedCheckConstraints != actualCheckConstraints
|
|
|| ExpectedForeignKeyConstraints != actualForeignKeyConstraints
|
|
|| ExpectedRoutines != actualRoutines
|
|
|| ExpectedViews != actualViews)
|
|
{
|
|
var err = $"E1030 - Database integrity check failed (C{actualColumns}:I{actualIndexes}:CC{actualCheckConstraints}:FC{actualForeignKeyConstraints}:V{actualViews}:R{actualRoutines})";
|
|
_log.LogCritical(err);
|
|
throw new ApplicationException(err);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
// Given a table name return the count of records in that table
|
|
// Used for metrics
|
|
//
|
|
///
|
|
internal static async Task<long> CountOfRecordsAsync(string TableName)
|
|
{
|
|
long ret = 0;
|
|
|
|
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
|
|
{
|
|
await conn.OpenAsync();
|
|
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
command.CommandText = $"SELECT count(*) FROM {TableName}";
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
await result.ReadAsync();
|
|
ret = result.GetInt64(0);
|
|
}
|
|
}
|
|
}
|
|
await conn.CloseAsync();
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////
|
|
// Returns all table names that are ours in current schema
|
|
//
|
|
///
|
|
internal static async Task<List<string>> GetAllTablenamesAsync()
|
|
{
|
|
|
|
List<string> ret = new List<string>();
|
|
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
|
|
{
|
|
await conn.OpenAsync();
|
|
|
|
using (var command = conn.CreateCommand())
|
|
{
|
|
command.CommandText = "SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';";
|
|
using (var result = await command.ExecuteReaderAsync())
|
|
{
|
|
if (result.HasRows)
|
|
{
|
|
while (await result.ReadAsync())
|
|
{
|
|
ret.Add(result.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
await conn.CloseAsync();
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
}//eoc
|
|
|
|
}//eons |