PostgreSql üzerinde çalışması için çalışma
This commit is contained in:
parent
0b5eb3d978
commit
01e19ef26f
47 changed files with 10764 additions and 8303 deletions
|
|
@ -15,7 +15,7 @@ public static class SettingsConsts
|
|||
|
||||
public const string FormNamePrefix = "Setting_";
|
||||
|
||||
public const string DefaultDatabaseProvider = DatabaseProvider.SqlServer;
|
||||
public const string DefaultDatabaseProvider = DatabaseProvider.PostgreSql;
|
||||
|
||||
public static class DatabaseProvider
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ public class DatabaseTableDto
|
|||
{
|
||||
public string SchemaName { get; set; }
|
||||
public string TableName { get; set; }
|
||||
public string FullName => $"{SchemaName}.{TableName}";
|
||||
public string DataSourceType { get; set; }
|
||||
public string FullName => DataSourceType == "Postgresql"
|
||||
? $"\"{SchemaName}\".\"{TableName}\""
|
||||
: $"[{SchemaName}].[{TableName}]";
|
||||
}
|
||||
|
||||
public class DatabaseColumnDto
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public interface ISqlObjectManagerAppService : IApplicationService
|
|||
|
||||
// Database Metadata Operations
|
||||
Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName);
|
||||
Task<string> GetTableCreateScriptAsync(string dataSourceCode, string schemaName, string tableName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL definition/body of a native SQL Server object (Stored Procedure, View, or Function)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ public class SqlNativeObjectDto
|
|||
{
|
||||
public string SchemaName { get; set; } = "dbo";
|
||||
public string ObjectName { get; set; } = "";
|
||||
public string FullName => $"[{SchemaName}].[{ObjectName}]";
|
||||
public string DataSourceType { get; set; } = "";
|
||||
public string FullName => DataSourceType == "Postgresql"
|
||||
? $"\"{SchemaName}\".\"{ObjectName}\""
|
||||
: $"[{SchemaName}].[{ObjectName}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
|
|||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using System.Text.RegularExpressions;
|
||||
using Sozsoft.Platform.Enums;
|
||||
using Sozsoft.Platform.Queries;
|
||||
|
||||
namespace Sozsoft.SqlQueryManager.Application;
|
||||
|
||||
|
|
@ -43,6 +45,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
|
||||
private readonly ISqlExecutorService _sqlExecutorService;
|
||||
private readonly ISqlTemplateProvider _templateProvider;
|
||||
private readonly IDataSourceManager _dataSourceManager;
|
||||
private readonly ICurrentTenant _currentTenant;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
|
|
@ -51,6 +54,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
public SqlObjectManagerAppService(
|
||||
ISqlExecutorService sqlExecutorService,
|
||||
ISqlTemplateProvider templateProvider,
|
||||
IDataSourceManager dataSourceManager,
|
||||
ICurrentTenant currentTenant,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IHostEnvironment hostEnvironment,
|
||||
|
|
@ -58,6 +62,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
{
|
||||
_sqlExecutorService = sqlExecutorService;
|
||||
_templateProvider = templateProvider;
|
||||
_dataSourceManager = dataSourceManager;
|
||||
_currentTenant = currentTenant;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_hostEnvironment = hostEnvironment;
|
||||
|
|
@ -90,11 +95,12 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
{
|
||||
ValidateTenantAccess();
|
||||
var result = new SqlObjectExplorerDto();
|
||||
var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
|
||||
|
||||
result.Tables = await GetTablesAsync(dataSourceCode);
|
||||
result.Views = await GetNativeObjectsAsync(dataSourceCode, "V");
|
||||
result.StoredProcedures = await GetNativeObjectsAsync(dataSourceCode, "P");
|
||||
result.Functions = await GetNativeObjectsAsync(dataSourceCode, "FN", "IF", "TF");
|
||||
result.Tables = await GetTablesAsync(dataSourceCode, dataSourceType);
|
||||
result.Views = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "V");
|
||||
result.StoredProcedures = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "P");
|
||||
result.Functions = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "FN", "IF", "TF");
|
||||
|
||||
result.Templates = _templateProvider.GetAvailableQueryTemplates()
|
||||
.Select(t => new SqlTemplateDto
|
||||
|
|
@ -109,20 +115,25 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
return result;
|
||||
}
|
||||
|
||||
private async Task<List<SqlNativeObjectDto>> GetNativeObjectsAsync(string dataSourceCode, params string[] objectTypes)
|
||||
private async Task<DataSourceTypeEnum> GetDataSourceTypeAsync(string dataSourceCode)
|
||||
{
|
||||
var typeList = string.Join(",", objectTypes.Select(t => $"'{t}'"));
|
||||
var query = $@"
|
||||
SELECT
|
||||
SCHEMA_NAME(o.schema_id) AS SchemaName,
|
||||
o.name AS ObjectName
|
||||
FROM
|
||||
sys.objects o
|
||||
WHERE
|
||||
o.type IN ({typeList})
|
||||
AND o.is_ms_shipped = 0
|
||||
ORDER BY
|
||||
SCHEMA_NAME(o.schema_id), o.name";
|
||||
var dataSource = await _dataSourceManager.GetDataSourceAsync(_currentTenant.IsAvailable, dataSourceCode);
|
||||
if (dataSource == null)
|
||||
{
|
||||
throw new Volo.Abp.UserFriendlyException($"Data source '{dataSourceCode}' was not found.");
|
||||
}
|
||||
|
||||
return dataSource.DataSourceType;
|
||||
}
|
||||
|
||||
private async Task<List<SqlNativeObjectDto>> GetNativeObjectsAsync(
|
||||
string dataSourceCode,
|
||||
DataSourceTypeEnum dataSourceType,
|
||||
params string[] objectTypes)
|
||||
{
|
||||
var query = dataSourceType == DataSourceTypeEnum.Postgresql
|
||||
? BuildPostgreSqlNativeObjectsQuery(objectTypes)
|
||||
: BuildSqlServerNativeObjectsQuery(objectTypes);
|
||||
|
||||
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode);
|
||||
|
||||
|
|
@ -136,8 +147,9 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
{
|
||||
objects.Add(new SqlNativeObjectDto
|
||||
{
|
||||
SchemaName = dict["SchemaName"]?.ToString() ?? "dbo",
|
||||
ObjectName = dict["ObjectName"]?.ToString() ?? ""
|
||||
SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? GetDefaultSchemaName(dataSourceType),
|
||||
ObjectName = GetDictionaryValue(dict, "ObjectName")?.ToString() ?? "",
|
||||
DataSourceType = dataSourceType.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -146,9 +158,73 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
return objects;
|
||||
}
|
||||
|
||||
private async Task<List<DatabaseTableDto>> GetTablesAsync(string dataSourceCode)
|
||||
private static string BuildSqlServerNativeObjectsQuery(params string[] objectTypes)
|
||||
{
|
||||
var query = @"
|
||||
var typeList = string.Join(",", objectTypes.Select(t => $"'{t}'"));
|
||||
return $@"
|
||||
SELECT
|
||||
SCHEMA_NAME(o.schema_id) AS SchemaName,
|
||||
o.name AS ObjectName
|
||||
FROM
|
||||
sys.objects o
|
||||
WHERE
|
||||
o.type IN ({typeList})
|
||||
AND o.is_ms_shipped = 0
|
||||
ORDER BY
|
||||
SCHEMA_NAME(o.schema_id), o.name";
|
||||
}
|
||||
|
||||
private static string BuildPostgreSqlNativeObjectsQuery(params string[] objectTypes)
|
||||
{
|
||||
var wantsViews = objectTypes.Contains("V");
|
||||
var wantsProcedures = objectTypes.Contains("P");
|
||||
var wantsFunctions = objectTypes.Any(t => t is "FN" or "IF" or "TF");
|
||||
|
||||
if (wantsViews)
|
||||
{
|
||||
return @"
|
||||
SELECT
|
||||
table_schema AS ""SchemaName"",
|
||||
table_name AS ""ObjectName""
|
||||
FROM information_schema.views
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY table_schema, table_name";
|
||||
}
|
||||
|
||||
var proKinds = new List<string>();
|
||||
if (wantsProcedures)
|
||||
{
|
||||
proKinds.Add("'p'");
|
||||
}
|
||||
|
||||
if (wantsFunctions)
|
||||
{
|
||||
proKinds.Add("'f'");
|
||||
}
|
||||
|
||||
return $@"
|
||||
SELECT
|
||||
n.nspname AS ""SchemaName"",
|
||||
p.proname AS ""ObjectName""
|
||||
FROM pg_proc p
|
||||
INNER JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND p.prokind IN ({string.Join(",", proKinds)})
|
||||
ORDER BY n.nspname, p.proname";
|
||||
}
|
||||
|
||||
private async Task<List<DatabaseTableDto>> GetTablesAsync(string dataSourceCode, DataSourceTypeEnum dataSourceType)
|
||||
{
|
||||
var query = dataSourceType == DataSourceTypeEnum.Postgresql
|
||||
? @"
|
||||
SELECT
|
||||
table_schema AS ""SchemaName"",
|
||||
table_name AS ""TableName""
|
||||
FROM information_schema.tables
|
||||
WHERE table_type = 'BASE TABLE'
|
||||
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY table_schema, table_name"
|
||||
: @"
|
||||
SELECT
|
||||
SCHEMA_NAME(t.schema_id) AS SchemaName,
|
||||
t.name AS TableName
|
||||
|
|
@ -171,8 +247,9 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
{
|
||||
tables.Add(new DatabaseTableDto
|
||||
{
|
||||
SchemaName = dict["SchemaName"]?.ToString() ?? "dbo",
|
||||
TableName = dict["TableName"]?.ToString() ?? ""
|
||||
SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? GetDefaultSchemaName(dataSourceType),
|
||||
TableName = GetDictionaryValue(dict, "TableName")?.ToString() ?? "",
|
||||
DataSourceType = dataSourceType.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -229,12 +306,9 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
public async Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName)
|
||||
{
|
||||
ValidateTenantAccess();
|
||||
var query = @"
|
||||
SELECT OBJECT_DEFINITION(OBJECT_ID(@ObjectName)) AS Definition";
|
||||
|
||||
var fullObjectName = $"[{schemaName}].[{objectName}]";
|
||||
var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
|
||||
var result = await _sqlExecutorService.ExecuteQueryAsync(
|
||||
query.Replace("@ObjectName", $"'{fullObjectName}'"),
|
||||
BuildNativeObjectDefinitionQuery(dataSourceType, schemaName, objectName),
|
||||
dataSourceCode);
|
||||
|
||||
if (result.Success && result.Data != null)
|
||||
|
|
@ -243,15 +317,18 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
if (dataList.Count > 0)
|
||||
{
|
||||
var row = dataList[0] as IDictionary<string, object>;
|
||||
if (row != null && row.ContainsKey("Definition"))
|
||||
if (row != null)
|
||||
{
|
||||
var definition = row["Definition"]?.ToString() ?? string.Empty;
|
||||
var definition = GetDictionaryValue(row, "Definition")?.ToString() ?? string.Empty;
|
||||
// Always open object script as CREATE OR ALTER in editor.
|
||||
if (dataSourceType == DataSourceTypeEnum.Mssql)
|
||||
{
|
||||
definition = Regex.Replace(
|
||||
definition,
|
||||
@"^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?",
|
||||
"CREATE OR ALTER ",
|
||||
RegexOptions.IgnoreCase);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
|
@ -260,10 +337,59 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string BuildNativeObjectDefinitionQuery(
|
||||
DataSourceTypeEnum dataSourceType,
|
||||
string schemaName,
|
||||
string objectName)
|
||||
{
|
||||
if (dataSourceType == DataSourceTypeEnum.Postgresql)
|
||||
{
|
||||
var schema = ToSqlLiteral(schemaName);
|
||||
var name = ToSqlLiteral(objectName);
|
||||
|
||||
return $@"
|
||||
SELECT ""Definition""
|
||||
FROM (
|
||||
SELECT pg_get_viewdef(c.oid, true) AS ""Definition""
|
||||
FROM pg_class c
|
||||
INNER JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = {schema}
|
||||
AND c.relname = {name}
|
||||
AND c.relkind IN ('v', 'm')
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT pg_get_functiondef(p.oid) AS ""Definition""
|
||||
FROM pg_proc p
|
||||
INNER JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE n.nspname = {schema}
|
||||
AND p.proname = {name}
|
||||
AND p.prokind IN ('f', 'p')
|
||||
) d
|
||||
LIMIT 1";
|
||||
}
|
||||
|
||||
var fullObjectName = $"[{schemaName.Replace("]", "]]")}].[{objectName.Replace("]", "]]")}]";
|
||||
return $@"
|
||||
SELECT OBJECT_DEFINITION(OBJECT_ID({ToSqlLiteral(fullObjectName)})) AS Definition";
|
||||
}
|
||||
|
||||
public async Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName)
|
||||
{
|
||||
ValidateTenantAccess();
|
||||
var query = $@"
|
||||
var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
|
||||
var query = dataSourceType == DataSourceTypeEnum.Postgresql
|
||||
? $@"
|
||||
SELECT
|
||||
column_name AS ""ColumnName"",
|
||||
COALESCE(NULLIF(udt_name, ''), data_type) AS ""DataType"",
|
||||
CASE WHEN is_nullable = 'YES' THEN TRUE ELSE FALSE END AS ""IsNullable"",
|
||||
character_maximum_length AS ""MaxLength""
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = {ToSqlLiteral(schemaName)}
|
||||
AND table_name = {ToSqlLiteral(tableName)}
|
||||
ORDER BY ordinal_position"
|
||||
: $@"
|
||||
SELECT
|
||||
c.name AS ColumnName,
|
||||
TYPE_NAME(c.user_type_id) AS DataType,
|
||||
|
|
@ -274,8 +400,8 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
INNER JOIN sys.tables t ON c.object_id = t.object_id
|
||||
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
WHERE
|
||||
s.name = '{schemaName}'
|
||||
AND t.name = '{tableName}'
|
||||
s.name = {ToSqlLiteral(schemaName)}
|
||||
AND t.name = {ToSqlLiteral(tableName)}
|
||||
ORDER BY
|
||||
c.column_id";
|
||||
|
||||
|
|
@ -291,10 +417,10 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
{
|
||||
columns.Add(new DatabaseColumnDto
|
||||
{
|
||||
ColumnName = dict["ColumnName"]?.ToString() ?? "",
|
||||
DataType = dict["DataType"]?.ToString() ?? "",
|
||||
IsNullable = dict["IsNullable"] is bool b && b,
|
||||
MaxLength = dict["MaxLength"] != null ? int.Parse(dict["MaxLength"].ToString()) : null
|
||||
ColumnName = GetDictionaryValue(dict, "ColumnName")?.ToString() ?? "",
|
||||
DataType = GetDictionaryValue(dict, "DataType")?.ToString() ?? "",
|
||||
IsNullable = ToBoolean(GetDictionaryValue(dict, "IsNullable")),
|
||||
MaxLength = ToNullableInt(GetDictionaryValue(dict, "MaxLength"))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -303,6 +429,218 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
|||
return columns;
|
||||
}
|
||||
|
||||
[HttpGet("api/app/sql-object-manager/table-create-script")]
|
||||
public async Task<string> GetTableCreateScriptAsync(string dataSourceCode, string schemaName, string tableName)
|
||||
{
|
||||
ValidateTenantAccess();
|
||||
var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
|
||||
|
||||
var result = await _sqlExecutorService.ExecuteQueryAsync(
|
||||
BuildTableCreateScriptQuery(dataSourceType, schemaName, tableName),
|
||||
dataSourceCode);
|
||||
|
||||
if (!result.Success || result.Data == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var row = result.Data.FirstOrDefault() as IDictionary<string, object>;
|
||||
return GetDictionaryValue(row, "Script")?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string BuildTableCreateScriptQuery(
|
||||
DataSourceTypeEnum dataSourceType,
|
||||
string schemaName,
|
||||
string tableName)
|
||||
{
|
||||
if (dataSourceType == DataSourceTypeEnum.Postgresql)
|
||||
{
|
||||
return BuildPostgreSqlTableCreateScriptQuery(schemaName, tableName);
|
||||
}
|
||||
|
||||
return BuildSqlServerTableCreateScriptQuery(schemaName, tableName);
|
||||
}
|
||||
|
||||
private static string BuildSqlServerTableCreateScriptQuery(string schemaName, string tableName)
|
||||
{
|
||||
var fullName = $"[{schemaName.Replace("]", "]]")}].[{tableName.Replace("]", "]]")}]";
|
||||
var escapedFullName = ToSqlLiteral(fullName);
|
||||
|
||||
return $@"
|
||||
DECLARE @ObjectId INT = OBJECT_ID({escapedFullName});
|
||||
|
||||
IF @ObjectId IS NULL
|
||||
BEGIN
|
||||
SELECT CAST('' AS NVARCHAR(MAX)) AS Script;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
;WITH cols AS
|
||||
(
|
||||
SELECT
|
||||
c.column_id,
|
||||
' ' + QUOTENAME(c.name) + ' ' +
|
||||
CASE
|
||||
WHEN t.name IN ('varchar', 'char', 'varbinary', 'binary') THEN
|
||||
t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR(10)) END + ')'
|
||||
WHEN t.name IN ('nvarchar', 'nchar') THEN
|
||||
t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length / 2 AS VARCHAR(10)) END + ')'
|
||||
WHEN t.name IN ('decimal', 'numeric') THEN
|
||||
t.name + '(' + CAST(c.precision AS VARCHAR(10)) + ',' + CAST(c.scale AS VARCHAR(10)) + ')'
|
||||
WHEN t.name IN ('datetime2', 'datetimeoffset', 'time') THEN
|
||||
t.name + '(' + CAST(c.scale AS VARCHAR(10)) + ')'
|
||||
ELSE t.name
|
||||
END +
|
||||
CASE
|
||||
WHEN ic.object_id IS NOT NULL
|
||||
THEN ' IDENTITY(' + CAST(ic.seed_value AS VARCHAR(30)) + ',' + CAST(ic.increment_value AS VARCHAR(30)) + ')'
|
||||
ELSE ''
|
||||
END +
|
||||
CASE WHEN c.is_nullable = 1 THEN ' NULL' ELSE ' NOT NULL' END +
|
||||
ISNULL(' DEFAULT ' + dc.definition, '') AS line
|
||||
FROM sys.columns c
|
||||
INNER JOIN sys.types t ON c.user_type_id = t.user_type_id
|
||||
LEFT JOIN sys.identity_columns ic ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
||||
LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id
|
||||
WHERE c.object_id = @ObjectId
|
||||
),
|
||||
pk AS
|
||||
(
|
||||
SELECT
|
||||
' CONSTRAINT ' + QUOTENAME(k.name) + ' PRIMARY KEY ' +
|
||||
CASE WHEN i.type = 1 THEN 'CLUSTERED' ELSE 'NONCLUSTERED' END +
|
||||
CHAR(13) + CHAR(10) + ' (' + CHAR(13) + CHAR(10) +
|
||||
(
|
||||
SELECT ' ' + QUOTENAME(c.name) + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' END + CHAR(13) + CHAR(10)
|
||||
FROM sys.index_columns ic
|
||||
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE ic.object_id = i.object_id
|
||||
AND ic.index_id = i.index_id
|
||||
AND ic.is_included_column = 0
|
||||
ORDER BY ic.key_ordinal
|
||||
FOR XML PATH(''), TYPE
|
||||
).value('.', 'NVARCHAR(MAX)') +
|
||||
' )' AS line
|
||||
FROM sys.key_constraints k
|
||||
INNER JOIN sys.indexes i ON k.parent_object_id = i.object_id AND k.unique_index_id = i.index_id
|
||||
WHERE k.parent_object_id = @ObjectId
|
||||
AND k.type = 'PK'
|
||||
)
|
||||
SELECT
|
||||
'IF OBJECT_ID(N''{fullName.Replace("'", "''")}'', ''U'') IS NULL' + CHAR(13) + CHAR(10) +
|
||||
'BEGIN' + CHAR(13) + CHAR(10) +
|
||||
' CREATE TABLE {fullName}' + CHAR(13) + CHAR(10) +
|
||||
' (' + CHAR(13) + CHAR(10) +
|
||||
STUFF(
|
||||
(
|
||||
SELECT ',' + CHAR(13) + CHAR(10) + line
|
||||
FROM cols
|
||||
ORDER BY column_id
|
||||
FOR XML PATH(''), TYPE
|
||||
).value('.', 'NVARCHAR(MAX)')
|
||||
,1,3,'') +
|
||||
ISNULL(
|
||||
(
|
||||
SELECT ',' + CHAR(13) + CHAR(10) + line
|
||||
FROM pk
|
||||
),
|
||||
''
|
||||
) + CHAR(13) + CHAR(10) + ' )' + CHAR(13) + CHAR(10) + 'END' + CHAR(13) + CHAR(10) + 'GO' AS Script;";
|
||||
}
|
||||
|
||||
private static string BuildPostgreSqlTableCreateScriptQuery(string schemaName, string tableName)
|
||||
{
|
||||
return $@"
|
||||
WITH cols AS (
|
||||
SELECT
|
||||
c.ordinal_position,
|
||||
' ' || quote_ident(c.column_name) || ' ' ||
|
||||
CASE
|
||||
WHEN c.data_type = 'character varying' THEN 'varchar(' || c.character_maximum_length || ')'
|
||||
WHEN c.data_type = 'character' THEN 'char(' || c.character_maximum_length || ')'
|
||||
WHEN c.data_type = 'numeric' AND c.numeric_precision IS NOT NULL THEN 'numeric(' || c.numeric_precision || ',' || c.numeric_scale || ')'
|
||||
WHEN c.data_type = 'USER-DEFINED' THEN c.udt_name
|
||||
ELSE c.data_type
|
||||
END ||
|
||||
CASE WHEN c.is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END ||
|
||||
COALESCE(' DEFAULT ' || c.column_default, '') AS line
|
||||
FROM information_schema.columns c
|
||||
WHERE c.table_schema = {ToSqlLiteral(schemaName)}
|
||||
AND c.table_name = {ToSqlLiteral(tableName)}
|
||||
),
|
||||
pk AS (
|
||||
SELECT
|
||||
' CONSTRAINT ' || quote_ident(tc.constraint_name) || ' PRIMARY KEY (' ||
|
||||
string_agg(quote_ident(kcu.column_name), ', ' ORDER BY kcu.ordinal_position) || ')' AS line
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
AND tc.table_name = kcu.table_name
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
AND tc.table_schema = {ToSqlLiteral(schemaName)}
|
||||
AND tc.table_name = {ToSqlLiteral(tableName)}
|
||||
GROUP BY tc.constraint_name
|
||||
)
|
||||
SELECT
|
||||
'CREATE TABLE IF NOT EXISTS ' || quote_ident({ToSqlLiteral(schemaName)}) || '.' || quote_ident({ToSqlLiteral(tableName)}) || E'\n(\n' ||
|
||||
string_agg(line, E',\n' ORDER BY sort_order) ||
|
||||
E'\n);' AS ""Script""
|
||||
FROM (
|
||||
SELECT ordinal_position AS sort_order, line FROM cols
|
||||
UNION ALL
|
||||
SELECT 100000 AS sort_order, line FROM pk
|
||||
) s;";
|
||||
}
|
||||
|
||||
private static object GetDictionaryValue(IDictionary<string, object> row, string key)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (row.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var match = row.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
return string.IsNullOrEmpty(match.Key) ? null : match.Value;
|
||||
}
|
||||
|
||||
private static string GetDefaultSchemaName(DataSourceTypeEnum dataSourceType)
|
||||
{
|
||||
return dataSourceType == DataSourceTypeEnum.Postgresql ? "public" : "dbo";
|
||||
}
|
||||
|
||||
private static string ToSqlLiteral(string value)
|
||||
{
|
||||
return $"'{(value ?? string.Empty).Replace("'", "''")}'";
|
||||
}
|
||||
|
||||
private static bool ToBoolean(object value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
bool boolValue => boolValue,
|
||||
short shortValue => shortValue != 0,
|
||||
int intValue => intValue != 0,
|
||||
long longValue => longValue != 0,
|
||||
_ => bool.TryParse(value?.ToString(), out var parsed) && parsed
|
||||
};
|
||||
}
|
||||
|
||||
private static int? ToNullableInt(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return int.TryParse(value.ToString(), out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result, bool isDeployed = false)
|
||||
{
|
||||
return new SqlQueryExecutionResultDto
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Sozsoft.Platform.DynamicData;
|
||||
using Sozsoft.Platform.Enums;
|
||||
using Sozsoft.Platform.Queries;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Volo.Abp;
|
||||
|
|
@ -38,14 +39,25 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
|
|||
.WithData("DataSourceCode", dataSourceCode);
|
||||
}
|
||||
|
||||
// Get appropriate repository based on database type
|
||||
// For now, using MS SQL Server repository
|
||||
var repository = _serviceProvider.GetKeyedService<IDynamicDataRepository>("Ms");
|
||||
var repositoryKey = dataSource.DataSourceType switch
|
||||
{
|
||||
DataSourceTypeEnum.Mssql => "Ms",
|
||||
DataSourceTypeEnum.Postgresql => "Pg",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (repositoryKey == null)
|
||||
{
|
||||
throw new BusinessException("SqlQueryManager:DataSourceTypeNotSupported")
|
||||
.WithData("DatabaseType", dataSource.DataSourceType);
|
||||
}
|
||||
|
||||
var repository = _serviceProvider.GetKeyedService<IDynamicDataRepository>(repositoryKey);
|
||||
|
||||
if (repository == null)
|
||||
{
|
||||
throw new BusinessException("SqlQueryManager:RepositoryNotFound")
|
||||
.WithData("DatabaseType", "Ms");
|
||||
.WithData("DatabaseType", repositoryKey);
|
||||
}
|
||||
|
||||
return repository;
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ public class AnnouncementDto : FullAuditedEntityDto<Guid>
|
|||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
public string Excerpt { get; set; }
|
||||
public string Content { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string Category { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Excerpt { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public Guid? UserId { get; set; }
|
||||
public UserInfoViewModel User { get; set; }
|
||||
public UserInfoViewModel? User { get; set; }
|
||||
public DateTime PublishDate { get; set; }
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
public bool IsPinned { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public string Attachments { get; set; }
|
||||
public string Attachments { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class SocialPostDto : FullAuditedEntityDto<Guid>
|
|||
{
|
||||
public Guid? UserId { get; set; }
|
||||
public UserInfoViewModel? User { get; set; }
|
||||
public string Content { get; set; }
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public int LikeCount { get; set; }
|
||||
public bool IsLiked { get; set; }
|
||||
|
|
@ -17,14 +17,14 @@ public class SocialPostDto : FullAuditedEntityDto<Guid>
|
|||
|
||||
public SocialLocationDto? Location { get; set; }
|
||||
public SocialMediaDto? Media { get; set; }
|
||||
public List<SocialCommentDto> Comments { get; set; }
|
||||
public List<SocialLikeDto> Likes { get; set; }
|
||||
public List<SocialCommentDto> Comments { get; set; } = [];
|
||||
public List<SocialLikeDto> Likes { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SocialLocationDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid SocialPostId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Address { get; set; }
|
||||
public double? Lat { get; set; }
|
||||
public double? Lng { get; set; }
|
||||
|
|
@ -34,8 +34,8 @@ public class SocialLocationDto : FullAuditedEntityDto<Guid>
|
|||
public class SocialMediaDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid SocialPostId { get; set; }
|
||||
public string Type { get; set; } // image | video | poll
|
||||
public string[] Urls { get; set; }
|
||||
public string Type { get; set; } = string.Empty; // image | video | poll
|
||||
public string[] Urls { get; set; } = [];
|
||||
|
||||
// Poll Fields
|
||||
public string? PollQuestion { get; set; }
|
||||
|
|
@ -43,13 +43,13 @@ public class SocialMediaDto : FullAuditedEntityDto<Guid>
|
|||
public DateTime? PollEndsAt { get; set; }
|
||||
public string? PollUserVoteId { get; set; }
|
||||
|
||||
public List<SocialPollOptionDto> PollOptions { get; set; }
|
||||
public List<SocialPollOptionDto> PollOptions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SocialPollOptionDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid SocialMediaId { get; set; }
|
||||
public string Text { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public int Votes { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ public class SocialCommentDto : FullAuditedEntityDto<Guid>
|
|||
public Guid SocialPostId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public UserInfoViewModel? User { get; set; }
|
||||
public string Content { get; set; }
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SocialLikeDto : FullAuditedEntityDto<Guid>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,22 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
.MapDepartmentAndJobPositionAssignments(departmentDict, jobPositionDict);
|
||||
}
|
||||
|
||||
private async Task<UserInfoViewModel> GetDashboardFallbackUserAsync(
|
||||
IReadOnlyDictionary<Guid, string> departmentDict,
|
||||
IReadOnlyDictionary<Guid, JobPosition> jobPositionDict)
|
||||
{
|
||||
var normalizedAdmin = PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue;
|
||||
var user = await _identityUserRepository.FindByNormalizedUserNameAsync(normalizedAdmin)
|
||||
?? await _identityUserRepository.FindByNormalizedEmailAsync(normalizedAdmin);
|
||||
|
||||
if (user == null && CurrentUser.Id.HasValue)
|
||||
{
|
||||
user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value);
|
||||
}
|
||||
|
||||
return user != null ? MapUserInfoViewModel(user, departmentDict, jobPositionDict) : null;
|
||||
}
|
||||
|
||||
private async Task<List<EventDto>> GetUpcomingEventsAsync()
|
||||
{
|
||||
var queryable = await _eventRepository
|
||||
|
|
@ -166,6 +182,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
}
|
||||
|
||||
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
|
||||
var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict);
|
||||
|
||||
var users = await _identityUserRepository.GetListAsync();
|
||||
var userDict = users
|
||||
|
|
@ -178,7 +195,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
var result = new List<EventDto>();
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (!evt.UserId.HasValue || !userDict.TryGetValue(evt.UserId.Value, out var user))
|
||||
var user = evt.UserId.HasValue && userDict.TryGetValue(evt.UserId.Value, out var eventUser)
|
||||
? eventUser
|
||||
: fallbackUser;
|
||||
|
||||
if (user == null)
|
||||
continue;
|
||||
|
||||
var commentDtos = new List<EventCommentDto>();
|
||||
|
|
@ -346,17 +367,22 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
var announcementDtos = new List<AnnouncementDto>();
|
||||
|
||||
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
|
||||
var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict);
|
||||
|
||||
foreach (var announcement in announcements)
|
||||
{
|
||||
var dto = ObjectMapper.Map<Announcement, AnnouncementDto>(announcement);
|
||||
|
||||
var user = await _identityUserRepository.FindAsync(announcement.UserId ?? Guid.Empty);
|
||||
if (announcement.UserId.HasValue)
|
||||
{
|
||||
var user = await _identityUserRepository.FindAsync(announcement.UserId.Value);
|
||||
if (user != null)
|
||||
{
|
||||
dto.User = MapUserInfoViewModel(user, departmentDict, jobPositionDict);
|
||||
}
|
||||
}
|
||||
|
||||
dto.User ??= fallbackUser;
|
||||
announcementDtos.Add(dto);
|
||||
}
|
||||
|
||||
|
|
@ -433,8 +459,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
// Collect all unique user IDs to resolve in a single query
|
||||
var userIds = dtos
|
||||
.Select(p => p.UserId)
|
||||
.Union(dtos.SelectMany(p => p.Comments.Select(c => c.UserId)))
|
||||
.Union(dtos.SelectMany(p => p.Likes.Select(l => l.UserId)))
|
||||
.Union(dtos.SelectMany(p => (p.Comments ?? []).Select(c => c.UserId)))
|
||||
.Union(dtos.SelectMany(p => (p.Likes ?? []).Select(l => l.UserId)))
|
||||
.Where(id => id.HasValue)
|
||||
.Select(id => id!.Value)
|
||||
.Distinct()
|
||||
|
|
@ -454,11 +480,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
if (dto.UserId.HasValue && userMap.TryGetValue(dto.UserId.Value, out var postUser))
|
||||
dto.User = postUser;
|
||||
|
||||
foreach (var comment in dto.Comments)
|
||||
foreach (var comment in dto.Comments ?? [])
|
||||
if (comment.UserId.HasValue && userMap.TryGetValue(comment.UserId.Value, out var commentUser))
|
||||
comment.User = commentUser;
|
||||
|
||||
foreach (var like in dto.Likes)
|
||||
foreach (var like in dto.Likes ?? [])
|
||||
if (like.UserId.HasValue && userMap.TryGetValue(like.UserId.Value, out var likeUser))
|
||||
like.User = likeUser;
|
||||
}
|
||||
|
|
@ -467,7 +493,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
foreach (var dto in dtos)
|
||||
{
|
||||
dto.IsOwnPost = dto.UserId == CurrentUser.Id;
|
||||
dto.IsLiked = dto.Likes.Any(l => l.UserId == CurrentUser.Id);
|
||||
dto.IsLiked = dto.Likes?.Any(l => l.UserId == CurrentUser.Id) == true;
|
||||
}
|
||||
|
||||
await EnrichPollOptionsAsync(dtos);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using static Sozsoft.Settings.SettingsConsts;
|
||||
|
||||
namespace Sozsoft.Platform.DbMigrator;
|
||||
|
||||
|
|
@ -13,6 +14,8 @@ class Program
|
|||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
ConfigurePostgreSqlCompatibility();
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
|
|
@ -30,6 +33,14 @@ class Program
|
|||
await CreateHostBuilder(args).RunConsoleAsync();
|
||||
}
|
||||
|
||||
private static void ConfigurePostgreSqlCompatibility()
|
||||
{
|
||||
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
|
||||
{
|
||||
System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.AddAppSettingsSecretsJson()
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public class HangfireDbSchemaMigrator : IPlatformDbSchemaMigrator, ITransientDep
|
|||
|
||||
public async Task MigrateAsync()
|
||||
{
|
||||
if (DefaultDatabaseProvider != DatabaseProvider.SqlServer)
|
||||
{
|
||||
_logger.LogInformation("HangFire SQL Server schema migration skipped for database provider '{Provider}'.", DefaultDatabaseProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionString = _configuration.GetConnectionString(DefaultDatabaseProvider);
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@
|
|||
"code": "App.SiteManagement.General.NewMemberNotificationEmails",
|
||||
"nameKey": "App.SiteManagement.General.NewMemberNotificationEmails",
|
||||
"descriptionKey": "App.SiteManagement.General.NewMemberNotificationEmails.Description",
|
||||
"defaultValue": "system@sozsoft.com",
|
||||
"defaultValue": "SYSTEM@SOZSOFT.COM",
|
||||
"isVisibleToClients": false,
|
||||
"providers": "G|D",
|
||||
"isInherited": false,
|
||||
|
|
@ -274,7 +274,7 @@
|
|||
"code": "App.SiteManagement.General.TimedLoginEmails",
|
||||
"nameKey": "App.SiteManagement.General.TimedLoginEmails",
|
||||
"descriptionKey": "App.SiteManagement.General.TimedLoginEmails.Description",
|
||||
"defaultValue": "system@sozsoft.com",
|
||||
"defaultValue": "SYSTEM@SOZSOFT.COM",
|
||||
"isVisibleToClients": false,
|
||||
"providers": "G|D",
|
||||
"isInherited": false,
|
||||
|
|
@ -466,7 +466,7 @@
|
|||
"code": "Abp.Mailing.DefaultFromAddress",
|
||||
"nameKey": "Abp.Mailing.DefaultFromAddress",
|
||||
"descriptionKey": "Abp.Mailing.DefaultFromAddress.Description",
|
||||
"defaultValue": "system@sozsoft.com",
|
||||
"defaultValue": "SYSTEM@SOZSOFT.COM",
|
||||
"isVisibleToClients": false,
|
||||
"providers": "T|G|D",
|
||||
"isInherited": false,
|
||||
|
|
@ -482,7 +482,7 @@
|
|||
"code": "Abp.Mailing.Smtp.UserName",
|
||||
"nameKey": "Abp.Mailing.Smtp.UserName",
|
||||
"descriptionKey": "Abp.Mailing.Smtp.UserName.Description",
|
||||
"defaultValue": "system@sozsoft.com",
|
||||
"defaultValue": "SYSTEM@SOZSOFT.COM",
|
||||
"isVisibleToClients": false,
|
||||
"providers": "T|G|D",
|
||||
"isInherited": false,
|
||||
|
|
|
|||
|
|
@ -212,6 +212,11 @@ public class HostDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
.ToListAsync())
|
||||
.ToHashSet();
|
||||
|
||||
var countryGroupNames = (await dbCtx.Set<CountryGroup>()
|
||||
.Select(c => c.Name)
|
||||
.ToListAsync())
|
||||
.ToHashSet();
|
||||
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
|
||||
using var fs = File.OpenRead(Path.Combine("Seeds", "Countries.json"));
|
||||
|
|
@ -228,10 +233,19 @@ public class HostDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
if (!seenCodes.Add(item.Name) || existingCodes.Contains(item.Name))
|
||||
continue;
|
||||
|
||||
var groupName = string.IsNullOrWhiteSpace(item.GroupName)
|
||||
? null
|
||||
: item.GroupName.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(groupName) && !countryGroupNames.Contains(groupName))
|
||||
{
|
||||
groupName = null;
|
||||
}
|
||||
|
||||
buffer.Add(new Country(
|
||||
item.Name,
|
||||
item.Name,
|
||||
item.GroupName,
|
||||
groupName,
|
||||
item.Currency,
|
||||
item.PhoneCode,
|
||||
item.TaxLabel
|
||||
|
|
|
|||
|
|
@ -18440,6 +18440,12 @@
|
|||
"en": "Failed",
|
||||
"tr": "Başarısız"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.DynamicServices.TotalDescription",
|
||||
"en": "Number of failed services",
|
||||
"tr": "Başarısız servislerin sayısı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.DynamicServices.FailedDescription",
|
||||
|
|
|
|||
|
|
@ -4164,11 +4164,11 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Event)),
|
||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 500, true, true, true, true, false, true),
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 900, 500, true, true, true, true, false, true),
|
||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
|
||||
{
|
||||
new() {
|
||||
Order = 1, ColCount = 4, ColSpan = 1, ItemType = "group", Items =
|
||||
Order = 1, ColCount = 3, ColSpan = 1, ItemType = "group", Items =
|
||||
[
|
||||
new EditingFormItemDto { Order = 1, DataField = "CategoryId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 2, DataField = "TypeId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
|
|
@ -4178,7 +4178,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
new EditingFormItemDto { Order = 6, DataField = "UserId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 7, DataField = "Status", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 8, DataField = "ParticipantsCount", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
|
||||
new EditingFormItemDto { Order = 9, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea },
|
||||
new EditingFormItemDto { Order = 9, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox },
|
||||
new EditingFormItemDto { Order = 10, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions },
|
||||
]}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE OR REPLACE PROCEDURE public."Adm_T_DatabaseBackupAll"()
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'PostgreSQL backup must be performed with pg_dump/pg_dumpall or an external backup service.';
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE OR REPLACE PROCEDURE public."Adm_T_DatabaseBackupFilesDeleteAll"()
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'PostgreSQL backup file cleanup must be performed by the host/container scheduler.';
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('hangfire.job') IS NULL
|
||||
OR to_regclass('hangfire.jobparameter') IS NULL
|
||||
OR to_regclass('hangfire.state') IS NULL THEN
|
||||
RAISE NOTICE 'Hangfire PostgreSQL tables were not found. Sas_H_BackgroundWorker_JobFlow view creation skipped.';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
EXECUTE $view$
|
||||
CREATE OR REPLACE VIEW public."Sas_H_BackgroundWorker_JobFlow" AS
|
||||
SELECT
|
||||
j.id AS "Id",
|
||||
COALESCE(p."RecurringJobId", 'OneTimeJob') AS "JobName",
|
||||
COALESCE(prc."ProcessingAt", enq."EnqueuedAt", j.createdat) AS "ExecutionDate",
|
||||
CASE
|
||||
WHEN suc."DurationMs" IS NOT NULL THEN suc."DurationMs"
|
||||
WHEN prc."ProcessingAt" IS NOT NULL AND suc."SucceededAt" IS NOT NULL
|
||||
THEN (EXTRACT(EPOCH FROM (suc."SucceededAt" - prc."ProcessingAt")) * 1000)::bigint
|
||||
WHEN prc."ProcessingAt" IS NOT NULL AND fld."FailedAt" IS NOT NULL
|
||||
THEN (EXTRACT(EPOCH FROM (fld."FailedAt" - prc."ProcessingAt")) * 1000)::bigint
|
||||
ELSE NULL
|
||||
END AS "Duration",
|
||||
j.statename AS "Status",
|
||||
CASE
|
||||
WHEN j.statename = 'Failed' THEN COALESCE(
|
||||
fld."ExceptionMessage",
|
||||
fld."FailedReason",
|
||||
fld."ExceptionType",
|
||||
'Unknown error'
|
||||
)
|
||||
ELSE NULL
|
||||
END AS "FailureReason",
|
||||
fld."ExceptionType",
|
||||
fld."ExceptionDetails"
|
||||
FROM hangfire.job j
|
||||
LEFT JOIN hangfire.jobqueue jq
|
||||
ON jq.jobid = j.id
|
||||
LEFT JOIN LATERAL
|
||||
(
|
||||
SELECT
|
||||
MAX(CASE
|
||||
WHEN jp.name = 'RecurringJobId'
|
||||
THEN REPLACE(jp.value, '"', '')
|
||||
END) AS "RecurringJobId"
|
||||
FROM hangfire.jobparameter jp
|
||||
WHERE jp.jobid = j.id
|
||||
) p ON TRUE
|
||||
LEFT JOIN LATERAL
|
||||
(
|
||||
SELECT
|
||||
s.createdat AS "EnqueuedAt",
|
||||
s.reason AS "EnqueuedReason",
|
||||
s.data::jsonb ->> 'Queue' AS "QueueName"
|
||||
FROM hangfire.state s
|
||||
WHERE s.jobid = j.id
|
||||
AND s.name = 'Enqueued'
|
||||
ORDER BY s.createdat DESC
|
||||
LIMIT 1
|
||||
) enq ON TRUE
|
||||
LEFT JOIN LATERAL
|
||||
(
|
||||
SELECT
|
||||
s.createdat AS "ProcessingAt",
|
||||
s.reason AS "ProcessingReason",
|
||||
s.data::jsonb ->> 'ServerId' AS "ServerName",
|
||||
s.data::jsonb ->> 'WorkerId' AS "WorkerId"
|
||||
FROM hangfire.state s
|
||||
WHERE s.jobid = j.id
|
||||
AND s.name = 'Processing'
|
||||
ORDER BY s.createdat DESC
|
||||
LIMIT 1
|
||||
) prc ON TRUE
|
||||
LEFT JOIN LATERAL
|
||||
(
|
||||
SELECT
|
||||
s.createdat AS "SucceededAt",
|
||||
s.reason AS "SucceededReason",
|
||||
NULLIF(s.data::jsonb ->> 'PerformanceDuration', '')::bigint AS "DurationMs"
|
||||
FROM hangfire.state s
|
||||
WHERE s.jobid = j.id
|
||||
AND s.name = 'Succeeded'
|
||||
ORDER BY s.createdat DESC
|
||||
LIMIT 1
|
||||
) suc ON TRUE
|
||||
LEFT JOIN LATERAL
|
||||
(
|
||||
SELECT
|
||||
s.createdat AS "FailedAt",
|
||||
s.reason AS "FailedReason",
|
||||
s.data::jsonb ->> 'ExceptionType' AS "ExceptionType",
|
||||
s.data::jsonb ->> 'ExceptionMessage' AS "ExceptionMessage",
|
||||
s.data::jsonb ->> 'ExceptionDetails' AS "ExceptionDetails"
|
||||
FROM hangfire.state s
|
||||
WHERE s.jobid = j.id
|
||||
AND s.name = 'Failed'
|
||||
ORDER BY s.createdat DESC
|
||||
LIMIT 1
|
||||
) fld ON TRUE
|
||||
WHERE COALESCE(j.invocationdata::jsonb ->> 'Method', '') <> 'DoWorkAsync'
|
||||
$view$;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -9,6 +9,7 @@ using Sozsoft.Platform.EntityFrameworkCore;
|
|||
using Volo.Abp.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.EntityFrameworkCore;
|
||||
using static Sozsoft.Settings.SettingsConsts;
|
||||
|
||||
namespace Sozsoft.Platform.Data.Seeds;
|
||||
|
||||
|
|
@ -32,10 +33,11 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
|
||||
public async Task SeedAsync(DataSeedContext context)
|
||||
{
|
||||
var sqlDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", "SqlData");
|
||||
var dataDirectoryName = GetDataDirectoryName();
|
||||
var sqlDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", dataDirectoryName);
|
||||
if (!Directory.Exists(sqlDataPath))
|
||||
{
|
||||
_logger.LogInformation("Seeds/SqlData directory not found, skipping SqlDataSeeder.");
|
||||
_logger.LogInformation("Seeds/{DirectoryName} directory not found, skipping SqlDataSeeder.", dataDirectoryName);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -45,11 +47,15 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
|
||||
if (sqlFiles.Length == 0)
|
||||
{
|
||||
_logger.LogInformation("No .sql files found in Seeds/SqlData directory, skipping SqlDataSeeder.");
|
||||
_logger.LogInformation("No .sql files found in Seeds/{DirectoryName} directory, skipping SqlDataSeeder.", dataDirectoryName);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("SqlDataSeeder started. {Count} file(s) to be processed.", sqlFiles.Length);
|
||||
_logger.LogInformation(
|
||||
"SqlDataSeeder started for provider '{Provider}' from Seeds/{DirectoryName}. {Count} file(s) to be processed.",
|
||||
DefaultDatabaseProvider,
|
||||
dataDirectoryName,
|
||||
sqlFiles.Length);
|
||||
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
|
||||
|
|
@ -98,6 +104,13 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
_logger.LogInformation("SqlDataSeeder completed. {Count} file(s) processed.", sqlFiles.Length);
|
||||
}
|
||||
|
||||
private static string GetDataDirectoryName()
|
||||
{
|
||||
return DefaultDatabaseProvider == DatabaseProvider.PostgreSql
|
||||
? "PostgresData"
|
||||
: "SqlData";
|
||||
}
|
||||
|
||||
private static (string Action, string? ObjectName, string? ObjectType) ExtractSqlInfo(string sql)
|
||||
{
|
||||
var patterns = new[]
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@
|
|||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Seeds\PostgresData\*.sql">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ public static class PlatformConsts
|
|||
public const string AdminRoleName = "admin";
|
||||
public const string AdminNameDefaultValue = "Sedat";
|
||||
public const string AdminSurNameDefaultValue = "ÖZTÜRK";
|
||||
public const string AdminEmailDefaultValue = "system@sozsoft.com";
|
||||
public const string AdminEmailDefaultValue = "SYSTEM@SOZSOFT.COM";
|
||||
public const string AdminPasswordDefaultValue = "1q2w3E*";
|
||||
public const string AdminPhoneNumberDefaultValue = "05449476346";
|
||||
public const string AdminRocketUsernameDefaultValue = "sedat.ozturk";
|
||||
|
|
|
|||
|
|
@ -21,47 +21,57 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
|
||||
public async Task<List<object>?> GetEntityListAsync(string entityName)
|
||||
{
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
var isPostgreSql = IsPostgreSql(dbContext);
|
||||
var tableName = QuoteIdentifier(entityName, isPostgreSql);
|
||||
var idDeletedColumn = QuoteIdentifier("IsDeleted", isPostgreSql);
|
||||
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
|
||||
var query = hasIsDeleted
|
||||
? $"SELECT * FROM [{entityName}] WHERE IsDeleted = 0 OR IsDeleted IS NULL"
|
||||
: $"SELECT * FROM [{entityName}]";
|
||||
? $"SELECT * FROM {tableName} WHERE {idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL"
|
||||
: $"SELECT * FROM {tableName}";
|
||||
return await ExecuteRawQueryAsync(query);
|
||||
}
|
||||
|
||||
public async Task<object?> GetEntityByIdAsync(string entityName, Guid id)
|
||||
{
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
var isPostgreSql = IsPostgreSql(dbContext);
|
||||
var tableName = QuoteIdentifier(entityName, isPostgreSql);
|
||||
var idColumn = QuoteIdentifier("Id", isPostgreSql);
|
||||
var isDeletedColumn = QuoteIdentifier("IsDeleted", isPostgreSql);
|
||||
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
|
||||
var query = hasIsDeleted
|
||||
? $"SELECT * FROM [{entityName}] WHERE Id = '{id}' AND (IsDeleted = 0 OR IsDeleted IS NULL)"
|
||||
: $"SELECT * FROM [{entityName}] WHERE Id = '{id}'";
|
||||
? $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}' AND ({isDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {isDeletedColumn} IS NULL)"
|
||||
: $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}'";
|
||||
var result = await ExecuteRawQueryAsync(query);
|
||||
return result?.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<object?> CreateEntityAsync(string entityName, JsonElement data)
|
||||
{
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
var isPostgreSql = IsPostgreSql(dbContext);
|
||||
var newId = Guid.NewGuid();
|
||||
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
|
||||
var hasCreationTime = await ColumnExistsAsync(entityName, "CreationTime");
|
||||
|
||||
var columns = new List<string> { "[Id]" };
|
||||
var columns = new List<string> { QuoteIdentifier("Id", isPostgreSql) };
|
||||
var values = new List<string> { $"'{newId}'" };
|
||||
|
||||
if (hasCreationTime) { columns.Add("[CreationTime]"); values.Add("SYSUTCDATETIME()"); }
|
||||
if (hasIsDeleted) { columns.Add("[IsDeleted]"); values.Add("0"); }
|
||||
if (hasCreationTime) { columns.Add(QuoteIdentifier("CreationTime", isPostgreSql)); values.Add(UtcNowExpression(isPostgreSql)); }
|
||||
if (hasIsDeleted) { columns.Add(QuoteIdentifier("IsDeleted", isPostgreSql)); values.Add(FalseLiteral(isPostgreSql)); }
|
||||
|
||||
foreach (var prop in data.EnumerateObject())
|
||||
{
|
||||
if (prop.NameEquals("id") || prop.NameEquals("Id"))
|
||||
continue;
|
||||
|
||||
columns.Add($"[{prop.Name}]");
|
||||
values.Add(FormatValueForSql(prop.Value));
|
||||
columns.Add(QuoteIdentifier(prop.Name, isPostgreSql));
|
||||
values.Add(FormatValueForSql(prop.Value, isPostgreSql));
|
||||
}
|
||||
|
||||
var insertQuery = $"INSERT INTO [{entityName}] ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
||||
var insertQuery = $"INSERT INTO {QuoteIdentifier(entityName, isPostgreSql)} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
||||
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
await dbContext.Database.ExecuteSqlRawAsync(insertQuery);
|
||||
|
||||
return await GetEntityByIdAsync(entityName, newId);
|
||||
|
|
@ -69,6 +79,8 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
|
||||
public async Task<object?> UpdateEntityAsync(string entityName, Guid id, JsonElement data)
|
||||
{
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
var isPostgreSql = IsPostgreSql(dbContext);
|
||||
var existing = await GetEntityByIdAsync(entityName, id);
|
||||
if (existing == null)
|
||||
return null;
|
||||
|
|
@ -77,19 +89,18 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
|
||||
var hasLastModification = await ColumnExistsAsync(entityName, "LastModificationTime");
|
||||
if (hasLastModification)
|
||||
setParts.Add("[LastModificationTime] = SYSUTCDATETIME()");
|
||||
setParts.Add($"{QuoteIdentifier("LastModificationTime", isPostgreSql)} = {UtcNowExpression(isPostgreSql)}");
|
||||
|
||||
foreach (var prop in data.EnumerateObject())
|
||||
{
|
||||
if (prop.NameEquals("id") || prop.NameEquals("Id"))
|
||||
continue;
|
||||
|
||||
setParts.Add($"[{prop.Name}] = {FormatValueForSql(prop.Value)}");
|
||||
setParts.Add($"{QuoteIdentifier(prop.Name, isPostgreSql)} = {FormatValueForSql(prop.Value, isPostgreSql)}");
|
||||
}
|
||||
|
||||
var updateQuery = $"UPDATE [{entityName}] SET {string.Join(", ", setParts)} WHERE Id = '{id}'";
|
||||
var updateQuery = $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {string.Join(", ", setParts)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'";
|
||||
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
await dbContext.Database.ExecuteSqlRawAsync(updateQuery);
|
||||
|
||||
return await GetEntityByIdAsync(entityName, id);
|
||||
|
|
@ -102,6 +113,7 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
return false;
|
||||
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
var isPostgreSql = IsPostgreSql(dbContext);
|
||||
|
||||
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
|
||||
try
|
||||
|
|
@ -110,21 +122,21 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
{
|
||||
var hasDeletionTime = await ColumnExistsAsync(entityName, "DeletionTime");
|
||||
var softDeleteQuery = hasDeletionTime
|
||||
? $"UPDATE [{entityName}] SET [IsDeleted] = 1, [DeletionTime] = SYSUTCDATETIME() WHERE Id = '{id}'"
|
||||
: $"UPDATE [{entityName}] SET [IsDeleted] = 1 WHERE Id = '{id}'";
|
||||
? $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {QuoteIdentifier("IsDeleted", isPostgreSql)} = {TrueLiteral(isPostgreSql)}, {QuoteIdentifier("DeletionTime", isPostgreSql)} = {UtcNowExpression(isPostgreSql)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'"
|
||||
: $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {QuoteIdentifier("IsDeleted", isPostgreSql)} = {TrueLiteral(isPostgreSql)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'";
|
||||
var affected = await dbContext.Database.ExecuteSqlRawAsync(softDeleteQuery);
|
||||
return affected > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var hardDeleteQuery = $"DELETE FROM [{entityName}] WHERE Id = '{id}'";
|
||||
var hardDeleteQuery = $"DELETE FROM {QuoteIdentifier(entityName, isPostgreSql)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'";
|
||||
var affected = await dbContext.Database.ExecuteSqlRawAsync(hardDeleteQuery);
|
||||
return affected > 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
var hardDeleteQuery = $"DELETE FROM [{entityName}] WHERE Id = '{id}'";
|
||||
var hardDeleteQuery = $"DELETE FROM {QuoteIdentifier(entityName, isPostgreSql)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'";
|
||||
var affected = await dbContext.Database.ExecuteSqlRawAsync(hardDeleteQuery);
|
||||
return affected > 0;
|
||||
}
|
||||
|
|
@ -132,8 +144,11 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
|
||||
private async Task<bool> ColumnExistsAsync(string tableName, string columnName)
|
||||
{
|
||||
var query = $"SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{tableName}' AND COLUMN_NAME = '{columnName}'";
|
||||
var dbContext = await _dbContextProvider.GetDbContextAsync();
|
||||
var isPostgreSql = IsPostgreSql(dbContext);
|
||||
var query = isPostgreSql
|
||||
? $"SELECT COUNT(1) FROM information_schema.columns WHERE table_schema NOT IN ('pg_catalog', 'information_schema') AND table_name = '{tableName.Replace("'", "''")}' AND column_name = '{columnName.Replace("'", "''")}'"
|
||||
: $"SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{tableName.Replace("'", "''")}' AND COLUMN_NAME = '{columnName.Replace("'", "''")}'";
|
||||
var connection = dbContext.Database.GetDbConnection();
|
||||
await dbContext.Database.OpenConnectionAsync();
|
||||
try
|
||||
|
|
@ -181,14 +196,14 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
}
|
||||
}
|
||||
|
||||
private static string FormatValueForSql(JsonElement value)
|
||||
private static string FormatValueForSql(JsonElement value, bool isPostgreSql)
|
||||
{
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when value.TryGetInt64(out var l) => l.ToString(),
|
||||
JsonValueKind.Number when value.TryGetDecimal(out var d) => d.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
JsonValueKind.True => "1",
|
||||
JsonValueKind.False => "0",
|
||||
JsonValueKind.True => TrueLiteral(isPostgreSql),
|
||||
JsonValueKind.False => FalseLiteral(isPostgreSql),
|
||||
JsonValueKind.Null => "NULL",
|
||||
JsonValueKind.String when value.TryGetGuid(out var g) => $"'{g}'",
|
||||
JsonValueKind.String when value.TryGetDateTime(out var dt) => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
|
||||
|
|
@ -196,5 +211,32 @@ public class DynamicEntityManager : IDynamicEntityManager
|
|||
_ => "NULL",
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsPostgreSql(PlatformDbContext dbContext)
|
||||
{
|
||||
return dbContext.Database.ProviderName?.Contains("Npgsql", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
private static string QuoteIdentifier(string identifier, bool isPostgreSql)
|
||||
{
|
||||
return isPostgreSql
|
||||
? $"\"{identifier.Replace("\"", "\"\"")}\""
|
||||
: $"[{identifier.Replace("]", "]]")}]";
|
||||
}
|
||||
|
||||
private static string TrueLiteral(bool isPostgreSql)
|
||||
{
|
||||
return isPostgreSql ? "TRUE" : "1";
|
||||
}
|
||||
|
||||
private static string FalseLiteral(bool isPostgreSql)
|
||||
{
|
||||
return isPostgreSql ? "FALSE" : "0";
|
||||
}
|
||||
|
||||
private static string UtcNowExpression(bool isPostgreSql)
|
||||
{
|
||||
return isPostgreSql ? "NOW() AT TIME ZONE 'UTC'" : "SYSUTCDATETIME()";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Sozsoft.Platform.DynamicData;
|
||||
using Sozsoft.Platform;
|
||||
using Npgsql;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Threading;
|
||||
|
|
@ -20,6 +24,10 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
|
||||
private readonly Dictionary<string, DbTransaction> _transactions;
|
||||
private readonly Dictionary<string, NpgsqlConnection> _connections;
|
||||
private readonly HashSet<string> _registeredTransactions;
|
||||
private readonly HashSet<string> _registeredConnections;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public PgDynamicDataRepository(
|
||||
|
|
@ -30,6 +38,8 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
_cancellationTokenProvider = cancellationTokenProvider;
|
||||
_transactions = [];
|
||||
_connections = [];
|
||||
_registeredTransactions = [];
|
||||
_registeredConnections = [];
|
||||
}
|
||||
|
||||
private string BuildKey(string cs)
|
||||
|
|
@ -41,30 +51,56 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
private async Task<NpgsqlConnection> GetOrCreateConnectionAsync(string cs)
|
||||
{
|
||||
var key = BuildKey(cs);
|
||||
if (!_connections.TryGetValue(key, out var connection))
|
||||
NpgsqlConnection connection;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_connections.TryGetValue(key, out connection))
|
||||
{
|
||||
// varsa aynı connection'ı kullan
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = new NpgsqlConnection(cs);
|
||||
_connections[key] = connection;
|
||||
}
|
||||
}
|
||||
|
||||
// Lock dışında state yönetimi
|
||||
if (connection.State == ConnectionState.Broken)
|
||||
{
|
||||
connection.Close();
|
||||
}
|
||||
|
||||
if (connection.State != ConnectionState.Open)
|
||||
if (connection.State == ConnectionState.Closed)
|
||||
{
|
||||
await connection.OpenAsync(_cancellationTokenProvider.FallbackToProvider(default));
|
||||
}
|
||||
|
||||
// UoW tamamlandığında connection'ı kapatmak için tek seferlik kayıt
|
||||
if (_unitOfWorkManager.Current != null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_registeredConnections.Contains(key))
|
||||
{
|
||||
_registeredConnections.Add(key);
|
||||
|
||||
_unitOfWorkManager.Current.OnCompleted(async () =>
|
||||
{
|
||||
if (_connections.TryGetValue(key, out var conn))
|
||||
NpgsqlConnection conn = null;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_connections.TryGetValue(key, out conn))
|
||||
{
|
||||
_connections.Remove(key);
|
||||
}
|
||||
|
||||
_registeredConnections.Remove(key);
|
||||
}
|
||||
|
||||
if (conn != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (conn.State != ConnectionState.Closed)
|
||||
|
|
@ -80,6 +116,8 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
|
@ -88,44 +126,62 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
{
|
||||
var key = BuildKey(cs);
|
||||
|
||||
if (_transactions.TryGetValue(key, out var tx))
|
||||
lock (_lock)
|
||||
{
|
||||
if (tx?.Connection != null &&
|
||||
tx.Connection == con &&
|
||||
tx.Connection.State == ConnectionState.Open)
|
||||
if (_transactions.TryGetValue(key, out var existing))
|
||||
{
|
||||
return tx;
|
||||
if (existing?.Connection != null &&
|
||||
existing.Connection == con &&
|
||||
existing.Connection.State == ConnectionState.Open)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
try { tx?.Dispose(); } catch { }
|
||||
try { existing?.Dispose(); } catch { }
|
||||
_transactions.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
var newTx = await con.BeginTransactionAsync(_cancellationTokenProvider.FallbackToProvider(default));
|
||||
_transactions[key] = newTx;
|
||||
var newTransaction = await con.BeginTransactionAsync(_cancellationTokenProvider.FallbackToProvider(default));
|
||||
bool shouldRegister = false;
|
||||
|
||||
if (_unitOfWorkManager.Current != null)
|
||||
lock (_lock)
|
||||
{
|
||||
_transactions[key] = newTransaction;
|
||||
|
||||
if (!_registeredTransactions.Contains(key))
|
||||
{
|
||||
_registeredTransactions.Add(key);
|
||||
shouldRegister = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRegister && _unitOfWorkManager.Current != null)
|
||||
{
|
||||
_unitOfWorkManager.Current.AddTransactionApi(
|
||||
key,
|
||||
new DapperTransactionApi(newTx, _cancellationTokenProvider)
|
||||
new DapperTransactionApi(newTransaction, _cancellationTokenProvider)
|
||||
);
|
||||
|
||||
_unitOfWorkManager.Current.OnCompleted(() =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_transactions.Remove(key);
|
||||
_registeredTransactions.Remove(key);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
return newTx;
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
// ------------------ Dapper metotları ------------------
|
||||
|
||||
public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
var param = new DynamicParameters(parameters);
|
||||
var param = CreateDynamicParameters(parameters);
|
||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||
|
||||
|
|
@ -135,7 +191,7 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
|
||||
public virtual async Task<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
var param = new DynamicParameters(parameters);
|
||||
var param = CreateDynamicParameters(parameters);
|
||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||
|
||||
|
|
@ -144,7 +200,7 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
|
||||
public virtual async Task<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
var param = new DynamicParameters(parameters);
|
||||
var param = CreateDynamicParameters(parameters);
|
||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||
|
||||
|
|
@ -153,7 +209,7 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
|
||||
public virtual async Task<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
var param = new DynamicParameters(parameters);
|
||||
var param = CreateDynamicParameters(parameters);
|
||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||
|
||||
|
|
@ -184,13 +240,257 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
|
||||
public virtual async Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
var param = new DynamicParameters(parameters);
|
||||
var param = CreateDynamicParameters(parameters);
|
||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||
|
||||
return await dbConnection.ExecuteAsync(sql, param, transaction);
|
||||
}
|
||||
|
||||
private static DynamicParameters CreateDynamicParameters(Dictionary<string, object> parameters)
|
||||
{
|
||||
var dynamicParameters = new DynamicParameters();
|
||||
|
||||
if (parameters == null)
|
||||
{
|
||||
return dynamicParameters;
|
||||
}
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
dynamicParameters.Add(parameter.Key, NormalizeParameterValue(parameter.Value));
|
||||
}
|
||||
|
||||
return dynamicParameters;
|
||||
}
|
||||
|
||||
private static object NormalizeParameterValue(object value)
|
||||
{
|
||||
if (value == null || value == DBNull.Value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
return NormalizeJsonElement(jsonElement);
|
||||
}
|
||||
|
||||
if (value is Array array && value is not byte[])
|
||||
{
|
||||
return NormalizeArrayParameter(array);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static object NormalizeArrayParameter(Array values)
|
||||
{
|
||||
var normalizedValues = values
|
||||
.Cast<object>()
|
||||
.Select(NormalizeParameterValue)
|
||||
.Where(value => value != null && value != DBNull.Value)
|
||||
.ToArray();
|
||||
|
||||
if (normalizedValues.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
if (TryBuildGuidArray(normalizedValues, out var guidValues))
|
||||
{
|
||||
return guidValues;
|
||||
}
|
||||
|
||||
if (TryBuildIntArray(normalizedValues, out var intValues))
|
||||
{
|
||||
return intValues;
|
||||
}
|
||||
|
||||
if (TryBuildLongArray(normalizedValues, out var longValues))
|
||||
{
|
||||
return longValues;
|
||||
}
|
||||
|
||||
if (TryBuildDecimalArray(normalizedValues, out var decimalValues))
|
||||
{
|
||||
return decimalValues;
|
||||
}
|
||||
|
||||
if (TryBuildBoolArray(normalizedValues, out var boolValues))
|
||||
{
|
||||
return boolValues;
|
||||
}
|
||||
|
||||
if (TryBuildDateTimeOffsetArray(normalizedValues, out var dateTimeOffsetValues))
|
||||
{
|
||||
return dateTimeOffsetValues;
|
||||
}
|
||||
|
||||
var stringValues = normalizedValues.Select(value => value.ToString()).ToArray();
|
||||
|
||||
if (stringValues.Length == 1 && stringValues[0]?.Contains(PlatformConsts.MultiValueDelimiter) == true)
|
||||
{
|
||||
return stringValues[0].Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
return stringValues;
|
||||
}
|
||||
|
||||
private static object NormalizeJsonElement(JsonElement value)
|
||||
{
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => value.GetString(),
|
||||
JsonValueKind.Number when value.TryGetInt32(out var intValue) => intValue,
|
||||
JsonValueKind.Number when value.TryGetInt64(out var longValue) => longValue,
|
||||
JsonValueKind.Number when value.TryGetDecimal(out var decimalValue) => decimalValue,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
JsonValueKind.Undefined => null,
|
||||
_ => value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryBuildGuidArray(object[] values, out Guid[] result)
|
||||
{
|
||||
result = new Guid[values.Length];
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] is Guid guidValue)
|
||||
{
|
||||
result[i] = guidValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(values[i]?.ToString(), out result[i]))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryBuildIntArray(object[] values, out int[] result)
|
||||
{
|
||||
result = new int[values.Length];
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] is int intValue)
|
||||
{
|
||||
result[i] = intValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(values[i]?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result[i]))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryBuildLongArray(object[] values, out long[] result)
|
||||
{
|
||||
result = new long[values.Length];
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] is long longValue)
|
||||
{
|
||||
result[i] = longValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!long.TryParse(values[i]?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result[i]))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryBuildDecimalArray(object[] values, out decimal[] result)
|
||||
{
|
||||
result = new decimal[values.Length];
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] is decimal decimalValue)
|
||||
{
|
||||
result[i] = decimalValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(values[i]?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out result[i]))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryBuildBoolArray(object[] values, out bool[] result)
|
||||
{
|
||||
result = new bool[values.Length];
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] is bool boolValue)
|
||||
{
|
||||
result[i] = boolValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!bool.TryParse(values[i]?.ToString(), out result[i]))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryBuildDateTimeOffsetArray(object[] values, out DateTimeOffset[] result)
|
||||
{
|
||||
result = new DateTimeOffset[values.Length];
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] is DateTimeOffset dateTimeOffsetValue)
|
||||
{
|
||||
result[i] = dateTimeOffsetValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (values[i] is DateTime dateTimeValue)
|
||||
{
|
||||
result[i] = dateTimeValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!DateTimeOffset.TryParse(values[i]?.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result[i]))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------ Dispose ------------------
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -207,12 +507,15 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var tx in _transactions.Values)
|
||||
{
|
||||
try { tx?.Dispose(); } catch { }
|
||||
}
|
||||
_transactions.Clear();
|
||||
_registeredTransactions.Clear();
|
||||
|
||||
foreach (var connection in _connections.Values)
|
||||
{
|
||||
|
|
@ -233,6 +536,8 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
}
|
||||
}
|
||||
_connections.Clear();
|
||||
_registeredConnections.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Sozsoft.Platform.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using static Sozsoft.Settings.SettingsConsts;
|
||||
|
||||
namespace Sozsoft.Platform.EntityFrameworkCore;
|
||||
|
||||
|
|
@ -26,10 +27,17 @@ public class EntityFrameworkCorePlatformDbSchemaMigrator
|
|||
* current scope.
|
||||
*/
|
||||
|
||||
await _serviceProvider
|
||||
var database = _serviceProvider
|
||||
.GetRequiredService<PlatformDbContext>()
|
||||
.Database
|
||||
.MigrateAsync();
|
||||
.Database;
|
||||
|
||||
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
|
||||
{
|
||||
await database.EnsureCreatedAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await database.MigrateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ public class PlatformDbContext :
|
|||
IIdentityDbContext,
|
||||
ITenantManagementDbContext
|
||||
{
|
||||
private readonly bool _isPostgreSql;
|
||||
|
||||
#region Saas
|
||||
public DbSet<LogEntry> LogEntries { get; set; }
|
||||
public DbSet<Tenant> Tenants { get; set; }
|
||||
|
|
@ -144,14 +146,15 @@ public class PlatformDbContext :
|
|||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
|
||||
_isPostgreSql = options.Extensions.Any(extension =>
|
||||
extension.GetType().Namespace?.StartsWith("Npgsql.EntityFrameworkCore.PostgreSQL", StringComparison.Ordinal) == true);
|
||||
}
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
base.ConfigureConventions(configurationBuilder);
|
||||
|
||||
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
|
||||
if (_isPostgreSql)
|
||||
{
|
||||
configurationBuilder.Properties<string>().UseCollation("tr-x-icu");
|
||||
}
|
||||
|
|
@ -1447,5 +1450,23 @@ public class PlatformDbContext :
|
|||
|
||||
b.HasIndex(x => new { x.TenantId, x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
|
||||
});
|
||||
|
||||
ConfigureProviderSpecificModel(builder);
|
||||
}
|
||||
|
||||
private void ConfigureProviderSpecificModel(ModelBuilder builder)
|
||||
{
|
||||
if (!_isPostgreSql)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var index in builder.Model.GetEntityTypes().SelectMany(entityType => entityType.GetIndexes()))
|
||||
{
|
||||
if (index.GetFilter() == "[IsDeleted] = 0")
|
||||
{
|
||||
index.SetFilter("\"IsDeleted\" = FALSE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,27 +16,45 @@ public class PlatformDbContextFactory : IDesignTimeDbContextFactory<PlatformDbCo
|
|||
PlatformEfCoreEntityExtensionMappings.Configure();
|
||||
|
||||
var configuration = BuildConfiguration();
|
||||
var databaseProvider = GetDatabaseProvider(args);
|
||||
|
||||
var builder = new DbContextOptionsBuilder<PlatformDbContext>();
|
||||
|
||||
switch (DefaultDatabaseProvider)
|
||||
switch (databaseProvider)
|
||||
{
|
||||
case DatabaseProvider.PostgreSql:
|
||||
//AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); // PGSQL
|
||||
builder.UseNpgsql(configuration.GetConnectionString(DefaultDatabaseProvider));
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
builder.UseNpgsql(configuration.GetConnectionString(DatabaseProvider.PostgreSql));
|
||||
break;
|
||||
|
||||
case DatabaseProvider.SqlServer:
|
||||
builder.UseSqlServer(configuration.GetConnectionString(DefaultDatabaseProvider));
|
||||
builder.UseSqlServer(configuration.GetConnectionString(DatabaseProvider.SqlServer));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException("Unsupported database provider configured.");
|
||||
throw new InvalidOperationException($"Unsupported database provider configured: {databaseProvider}");
|
||||
}
|
||||
|
||||
return new PlatformDbContext(builder.Options);
|
||||
}
|
||||
|
||||
private static string GetDatabaseProvider(string[] args)
|
||||
{
|
||||
var provider = Environment.GetEnvironmentVariable("EF_DATABASE_PROVIDER");
|
||||
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i].Equals("--provider", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||
{
|
||||
provider = args[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(provider)
|
||||
? DefaultDatabaseProvider
|
||||
: provider;
|
||||
}
|
||||
|
||||
private static IConfigurationRoot BuildConfiguration()
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -202,7 +202,7 @@
|
|||
"Summary": "blog.posts.ai.excerpt",
|
||||
"CoverImage": "https://images.pexels.com/photos/8386434/pexels-photo-8386434.jpeg?auto=compress&cs=tinysrgb&w=1920",
|
||||
"CategoryName": "blog.categories.technology",
|
||||
"Author": "system@sozsoft.com"
|
||||
"Author": "SYSTEM@SOZSOFT.COM"
|
||||
},
|
||||
{
|
||||
"Title": "blog.posts.web.title",
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
"Summary": "blog.posts.web.excerpt",
|
||||
"CoverImage": "https://images.pexels.com/photos/11035471/pexels-photo-11035471.jpeg?auto=compress&cs=tinysrgb&w=1920",
|
||||
"CategoryName": "blog.categories.webdev",
|
||||
"Author": "system@sozsoft.com"
|
||||
"Author": "SYSTEM@SOZSOFT.COM"
|
||||
},
|
||||
{
|
||||
"Title": "blog.posts.security.title",
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
"Summary": "blog.posts.security.excerpt",
|
||||
"CoverImage": "https://images.pexels.com/photos/5380642/pexels-photo-5380642.jpeg?auto=compress&cs=tinysrgb&w=1920",
|
||||
"CategoryName": "blog.categories.security",
|
||||
"Author": "system@sozsoft.com"
|
||||
"Author": "SYSTEM@SOZSOFT.COM"
|
||||
},
|
||||
{
|
||||
"Title": "blog.posts.mobile.title",
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
"ReadTime": "4 dk",
|
||||
"CoverImage": "https://images.pexels.com/photos/13017583/pexels-photo-13017583.jpeg?auto=compress&cs=tinysrgb&w=1920",
|
||||
"CategoryName": "blog.categories.mobile",
|
||||
"Author": "system@sozsoft.com"
|
||||
"Author": "SYSTEM@SOZSOFT.COM"
|
||||
},
|
||||
{
|
||||
"Title": "blog.posts.database.title",
|
||||
|
|
@ -246,7 +246,7 @@
|
|||
"ReadTime": "8 dk",
|
||||
"CoverImage": "https://images.pexels.com/photos/325229/pexels-photo-325229.jpeg?auto=compress&cs=tinysrgb&w=1920",
|
||||
"CategoryName": "blog.categories.database",
|
||||
"Author": "system@sozsoft.com"
|
||||
"Author": "SYSTEM@SOZSOFT.COM"
|
||||
},
|
||||
{
|
||||
"Title": "blog.posts.digital.title",
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
"ReadTime": "6 dk",
|
||||
"CoverImage": "https://images.pexels.com/photos/7681091/pexels-photo-7681091.jpeg?auto=compress&cs=tinysrgb&w=1920",
|
||||
"CategoryName": "blog.categories.digital",
|
||||
"Author": "system@sozsoft.com"
|
||||
"Author": "SYSTEM@SOZSOFT.COM"
|
||||
}
|
||||
],
|
||||
"GlobalSearch": [
|
||||
|
|
@ -341,7 +341,7 @@
|
|||
"PermissionsJson": [
|
||||
{
|
||||
"ResourceType": "User",
|
||||
"ResourceId": "system@sozsoft.com"
|
||||
"ResourceId": "SYSTEM@SOZSOFT.COM"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1298,7 +1298,7 @@
|
|||
"content": "Ankara ofisimiz 1 Kasım tarihinde hizmete başlıyor! Tüm çalışanlarımızı açılış törenimize davet ediyoruz.",
|
||||
"excerpt": "Ankara ofisimiz 1 Kasım tarihinde hizmete başlıyor!",
|
||||
"category": "general",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"publishDate": "12-10-2024",
|
||||
"isPinned": true,
|
||||
"viewCount": 0,
|
||||
|
|
@ -1309,7 +1309,7 @@
|
|||
"content": "Yıl sonu performans değerlendirmelerimiz 20 Ekim - 5 Kasım tarihleri arasında gerçekleştirilecektir. Lütfen formları zamanında doldurunuz.",
|
||||
"excerpt": "Yıl sonu performans değerlendirmeleri başlıyor.",
|
||||
"category": "event",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"publishDate": "08-10-2024",
|
||||
"expiryDate": "05-11-2024",
|
||||
"isPinned": true,
|
||||
|
|
@ -1320,7 +1320,7 @@
|
|||
"content": "Bu Cumartesi saat 02: 00 - 06: 00 arası sistemlerimizde bakım çalışması yapılacaktır. Bu süre içinde sistemlere erişim sağlanamayacaktır.",
|
||||
"excerpt": "Cumartesi gecesi planlı bakım çalışması",
|
||||
"category": "urgent",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"publishDate": "08-10-2024",
|
||||
"isPinned": false,
|
||||
"viewCount": 0
|
||||
|
|
@ -1330,7 +1330,7 @@
|
|||
"content": "Yazılım Geliştirme ekibimiz için React İleri Seviye eğitimi 25-26 Ekim tarihlerinde düzenlenecektir. Katılım için IK birimine başvurunuz.",
|
||||
"excerpt": "React İleri Seviye eğitimi kayıtları başladı",
|
||||
"category": "event",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"publishDate": "09-10-2024",
|
||||
"isPinned": false,
|
||||
"viewCount": 0
|
||||
|
|
@ -1340,7 +1340,7 @@
|
|||
"content": "Bilgi güvenliği politikamız güncellenmiştir. Tüm çalışanlarımızın yeni politikayı okuması ve onaylaması gerekmektedir.",
|
||||
"excerpt": "Güvenlik politikası güncellendi - Onay gerekli",
|
||||
"category": "urgent",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"publishDate": "04-10-2024",
|
||||
"isPinned": true,
|
||||
"viewCount": 0
|
||||
|
|
@ -1514,42 +1514,42 @@
|
|||
"SocialPosts": [
|
||||
{
|
||||
"content": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"likeCount": 0,
|
||||
"isLiked": false,
|
||||
"isOwnPost": true
|
||||
},
|
||||
{
|
||||
"content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"likeCount": 0,
|
||||
"isLiked": false,
|
||||
"isOwnPost": true
|
||||
},
|
||||
{
|
||||
"content": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"likeCount": 0,
|
||||
"isLiked": false,
|
||||
"isOwnPost": true
|
||||
},
|
||||
{
|
||||
"content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"likeCount": 0,
|
||||
"isLiked": false,
|
||||
"isOwnPost": true
|
||||
},
|
||||
{
|
||||
"content": "Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"likeCount": 0,
|
||||
"isLiked": false,
|
||||
"isOwnPost": true
|
||||
},
|
||||
{
|
||||
"content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"likeCount": 0,
|
||||
"isLiked": false,
|
||||
"isOwnPost": true
|
||||
|
|
@ -1635,53 +1635,53 @@
|
|||
"SocialComments": [
|
||||
{
|
||||
"postContent": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Harika görünüyor! Başarılar 👏"
|
||||
},
|
||||
{
|
||||
"postContent": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "TypeScript gerçekten fark yaratıyor!"
|
||||
},
|
||||
{
|
||||
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Mesajlaşma özelliğine kesinlikle ihtiyacımız var!"
|
||||
},
|
||||
{
|
||||
"postContent": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Tasarımlar çok şık! Renk paleti özellikle güzel 😍"
|
||||
},
|
||||
{
|
||||
"postContent": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Dark mode opsiyonu da olacak mı?"
|
||||
},
|
||||
{
|
||||
"postContent": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Harika iş! Detayları paylaşabilir misin?"
|
||||
},
|
||||
{
|
||||
"postContent": "Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Ne zaman başlıyor?"
|
||||
},
|
||||
{
|
||||
"postContent": "Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.",
|
||||
"userName": "system@sozsoft.com",
|
||||
"userName": "SYSTEM@SOZSOFT.COM",
|
||||
"content": "Gelecek hafta başlıyoruz! Kayıt linki mail ile paylaşılacak."
|
||||
}
|
||||
],
|
||||
"SocialLikes": [
|
||||
{
|
||||
"postContent": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
|
||||
"userName": "system@sozsoft.com"
|
||||
"userName": "SYSTEM@SOZSOFT.COM"
|
||||
},
|
||||
{
|
||||
"postContent": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨",
|
||||
"userName": "system@sozsoft.com"
|
||||
"userName": "SYSTEM@SOZSOFT.COM"
|
||||
}
|
||||
],
|
||||
"EventTypes": [
|
||||
|
|
@ -1862,7 +1862,7 @@
|
|||
"Description": "Tüm departmanların katılımıyla düzenlenen geleneksel yaz futbol turnuvası.",
|
||||
"Place": "Şirket Kampüsü Spor Alanı",
|
||||
"Status": "published",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"ParticipantsCount": 64,
|
||||
"IsPublished": true,
|
||||
"Likes": 0,
|
||||
|
|
@ -1877,7 +1877,7 @@
|
|||
"Description": "Çalışanlarımıza özel, rehber eşliğinde 2 günlük kültürel gezi.",
|
||||
"Place": "Kapadokya, Nevşehir",
|
||||
"Status": "published",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"ParticipantsCount": 25,
|
||||
"IsPublished": true,
|
||||
"Likes": 0,
|
||||
|
|
@ -1892,7 +1892,7 @@
|
|||
"Description": "Caz müziğinin en güzel örneklerinin canlı performanslarla sunulacağı özel akşam.",
|
||||
"Place": "Şirket Konferans Salonu",
|
||||
"Status": "published",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"ParticipantsCount": 40,
|
||||
"IsPublished": true,
|
||||
"Likes": 0,
|
||||
|
|
@ -1903,55 +1903,55 @@
|
|||
"EventComments": [
|
||||
{
|
||||
"EventName": "Yaz Futbol Turnuvası 2025",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Muhteşem bir gündü! Yılın en güzel etkinliği 🎉",
|
||||
"Likes": 12
|
||||
},
|
||||
{
|
||||
"EventName": "Yaz Futbol Turnuvası 2025",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Voleybol turnuvası harikaydı, gelecek yıl yine yapalım!",
|
||||
"Likes": 8
|
||||
},
|
||||
{
|
||||
"EventName": "Kültür Gezisi: Kapadokya",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Ekibimiz 2. oldu! Çok gurur duydum herkesle 💪",
|
||||
"Likes": 15
|
||||
},
|
||||
{
|
||||
"EventName": "Kültür Gezisi: Kapadokya",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Gece boyunca kod yazmak ve pizza yemek priceless! 🍕",
|
||||
"Likes": 10
|
||||
},
|
||||
{
|
||||
"EventName": "Müzik Dinletisi: Jazz Akşamı",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "İT departmanı şampiyon oldu! Gelecek sene kupayı koruyacağız 🏆",
|
||||
"Likes": 18
|
||||
},
|
||||
{
|
||||
"EventName": "Müzik Dinletisi: Jazz Akşamı",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Yılın en şık gecesi! Organizasyon mükemmeldi 👏",
|
||||
"Likes": 25
|
||||
},
|
||||
{
|
||||
"EventName": "Müzik Dinletisi: Jazz Akşamı",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Tombala hediyelerim harika, çok teşekkürler! 🎁",
|
||||
"Likes": 14
|
||||
},
|
||||
{
|
||||
"EventName": "Müzik Dinletisi: Jazz Akşamı",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "Müzik grubunuz süperdi, dans pistinden ayrılamadık! 🎵",
|
||||
"Likes": 19
|
||||
},
|
||||
{
|
||||
"EventName": "Müzik Dinletisi: Jazz Akşamı",
|
||||
"UserName": "system@sozsoft.com",
|
||||
"UserName": "SYSTEM@SOZSOFT.COM",
|
||||
"Content": "İlk defa ebru yaptım, çok huzurlu bir deneyimdi 🎨",
|
||||
"Likes": 11
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Microsoft.Data.SqlClient;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Npgsql;
|
||||
using Serilog;
|
||||
using static Sozsoft.Settings.SettingsConsts;
|
||||
|
||||
|
|
@ -38,9 +39,10 @@ internal static class SetupAppRunner
|
|||
if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
|
||||
return SqlServerIsReady(connectionString);
|
||||
|
||||
#pragma warning disable CS0162
|
||||
return true; // Diğer sağlayıcılar için geçici — ileride PostgreSQL desteği eklenecek
|
||||
#pragma warning restore CS0162
|
||||
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
|
||||
return PostgreSqlIsReady(connectionString);
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -83,6 +85,46 @@ internal static class SetupAppRunner
|
|||
return (int)tableCheck.ExecuteScalar() > 0;
|
||||
}
|
||||
|
||||
private static bool PostgreSqlIsReady(string connectionString)
|
||||
{
|
||||
var csb = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
var dbName = csb.Database;
|
||||
if (string.IsNullOrEmpty(dbName))
|
||||
return false;
|
||||
|
||||
var maintenanceCsb = new NpgsqlConnectionStringBuilder(connectionString)
|
||||
{
|
||||
Database = "postgres",
|
||||
Timeout = 8,
|
||||
CommandTimeout = 8
|
||||
};
|
||||
|
||||
using var maintenanceConn = new NpgsqlConnection(maintenanceCsb.ConnectionString);
|
||||
maintenanceConn.Open();
|
||||
|
||||
using var dbCheck = new NpgsqlCommand(
|
||||
"SELECT COUNT(1) FROM pg_database WHERE datname = @n",
|
||||
maintenanceConn);
|
||||
dbCheck.Parameters.AddWithValue("n", dbName);
|
||||
if (Convert.ToInt32(dbCheck.ExecuteScalar()) == 0)
|
||||
return false;
|
||||
|
||||
csb.Timeout = 8;
|
||||
csb.CommandTimeout = 8;
|
||||
using var dbConn = new NpgsqlConnection(csb.ConnectionString);
|
||||
dbConn.Open();
|
||||
|
||||
using var tableCheck = new NpgsqlCommand(
|
||||
"""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
AND lower(table_name) = lower('AbpRoles')
|
||||
""",
|
||||
dbConn);
|
||||
return Convert.ToInt32(tableCheck.ExecuteScalar()) > 0;
|
||||
}
|
||||
|
||||
// Minimal Kurulum Uygulaması
|
||||
|
||||
public static async Task<int> RunAsync(string[] args, IConfiguration configuration)
|
||||
|
|
|
|||
|
|
@ -337,6 +337,12 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
|
||||
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
|
||||
if (connectionString.IsNullOrWhiteSpace() || !SetupAppRunner.DatabaseIsReady(configuration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Configure<AbpHangfireOptions>(options =>
|
||||
{
|
||||
options.ServerOptions = new BackgroundJobServerOptions
|
||||
|
|
@ -345,31 +351,25 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
};
|
||||
});
|
||||
|
||||
// Configure Hangfire storage based on database provider
|
||||
// Note: Currently DefaultDatabaseProvider is set to SqlServer in SettingsConsts.cs
|
||||
// PostgreSQL configuration is preserved for potential future use
|
||||
#pragma warning disable CS0162 // Unreachable code detected
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
|
||||
{
|
||||
context.Services.AddHangfire(options =>
|
||||
{
|
||||
options.UsePostgreSqlStorage(
|
||||
configuration.GetConnectionString(DefaultDatabaseProvider),
|
||||
storageOptions => storageOptions.UseNpgsqlConnection(connectionString),
|
||||
new PostgreSqlStorageOptions
|
||||
{
|
||||
PrepareSchemaIfNecessary = true
|
||||
});
|
||||
});
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
#pragma warning restore CS0162
|
||||
else if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
|
||||
{
|
||||
context.Services.AddHangfire(options =>
|
||||
{
|
||||
options.UseSqlServerStorage(
|
||||
configuration.GetConnectionString(DefaultDatabaseProvider),
|
||||
connectionString,
|
||||
new SqlServerStorageOptions
|
||||
{
|
||||
PrepareSchemaIfNecessary = true,
|
||||
|
|
@ -382,6 +382,7 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
});
|
||||
});
|
||||
}
|
||||
#pragma warning restore CS0162
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public class Program
|
|||
{
|
||||
public async static Task<int> Main(string[] args)
|
||||
{
|
||||
ConfigurePostgreSqlCompatibility();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
|
|
@ -191,6 +193,14 @@ public class Program
|
|||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigurePostgreSqlCompatibility()
|
||||
{
|
||||
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
|
||||
{
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
1
configs/docker/start_postgres.bat
Normal file
1
configs/docker/start_postgres.bat
Normal file
|
|
@ -0,0 +1 @@
|
|||
docker compose -f .\docker-compose-data.yml --profile postgres up -d
|
||||
1
configs/docker/stop_postgres.bat
Normal file
1
configs/docker/stop_postgres.bat
Normal file
|
|
@ -0,0 +1 @@
|
|||
docker compose -f .\docker-compose-data.yml --profile postgres down -v
|
||||
1
configs/docker/stop_sql.bat
Normal file
1
configs/docker/stop_sql.bat
Normal file
|
|
@ -0,0 +1 @@
|
|||
docker compose -f .\docker-compose-data.yml --profile sql down -v
|
||||
|
|
@ -1,6 +1,17 @@
|
|||
{
|
||||
"commit": "8f3932b",
|
||||
"commit": "0b5eb3d",
|
||||
"releases": [
|
||||
{
|
||||
"version": "1.1.01",
|
||||
"buildDate": "2026-05-24",
|
||||
"commit": "6262baa6f12d695a25d83304af985092715d439a",
|
||||
"changeLog": [
|
||||
"- Workflow tanımlaması yapılabilir.",
|
||||
"- Dark mod için uygulama güncellemesi",
|
||||
"- Form Field kısmında düzenleme",
|
||||
"- Hangfire Recurring Job düzenlemesi."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.10",
|
||||
"buildDate": "2026-05-11",
|
||||
|
|
|
|||
|
|
@ -27,12 +27,14 @@ export interface SqlTemplateDto {
|
|||
export interface DatabaseTableDto {
|
||||
schemaName: string
|
||||
tableName: string
|
||||
dataSourceType?: string
|
||||
fullName: string
|
||||
}
|
||||
|
||||
export interface SqlNativeObjectDto {
|
||||
schemaName: string
|
||||
objectName: string
|
||||
dataSourceType?: string
|
||||
fullName: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,16 @@ export class SqlObjectManagerService {
|
|||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
||||
getTableCreateScript = (dataSourceCode: string, schemaName: string, tableName: string, config?: Partial<Config>) =>
|
||||
apiService.fetchData<string, void>(
|
||||
{
|
||||
method: 'GET',
|
||||
url: '/api/app/sql-object-manager/table-create-script',
|
||||
params: { dataSourceCode, schemaName, tableName },
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
||||
getNativeObjectDefinition = (dataSourceCode: string, schemaName: string, objectName: string, config?: Partial<Config>) =>
|
||||
apiService.fetchData<string, void>(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
FaTrash,
|
||||
} from 'react-icons/fa'
|
||||
import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models'
|
||||
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ interface TreeNode {
|
|||
|
||||
interface SqlObjectExplorerProps {
|
||||
dataSource: string | null
|
||||
dataSourceType?: DataSourceTypeEnum
|
||||
onTemplateSelect?: (template: string, templateType: string) => void
|
||||
onViewDefinition?: (schemaName: string, objectName: string) => void
|
||||
onGenerateTableScript?: (schemaName: string, tableName: string) => void
|
||||
|
|
@ -55,6 +57,7 @@ const FOLDER_META: Record<FolderKey, { label: string; color: string }> = {
|
|||
|
||||
const SqlObjectExplorer = ({
|
||||
dataSource,
|
||||
dataSourceType,
|
||||
onTemplateSelect,
|
||||
onViewDefinition,
|
||||
onGenerateTableScript,
|
||||
|
|
@ -64,6 +67,7 @@ const SqlObjectExplorer = ({
|
|||
refreshTrigger,
|
||||
}: SqlObjectExplorerProps) => {
|
||||
const { translate } = useLocalization()
|
||||
const isPostgreSql = dataSourceType === DataSourceTypeEnum.Postgresql
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set(['root']))
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
|
@ -182,7 +186,13 @@ const SqlObjectExplorer = ({
|
|||
if (node.folder === 'tables') {
|
||||
// Generate SELECT template for tables
|
||||
const t = node.data as DatabaseTableDto
|
||||
onTemplateSelect?.(`SELECT TOP 10 *\nFROM ${t.fullName ?? `[${t.schemaName}].[${t.tableName}]`};`, 'table-select')
|
||||
const fullName = t.fullName ?? (isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
|
||||
onTemplateSelect?.(
|
||||
isPostgreSql
|
||||
? `SELECT *\nFROM ${fullName}\nLIMIT 10;`
|
||||
: `SELECT TOP 10 *\nFROM ${fullName};`,
|
||||
'table-select',
|
||||
)
|
||||
} else {
|
||||
// Load native object definition into editor
|
||||
const obj = node.data as SqlNativeObjectDto
|
||||
|
|
@ -250,14 +260,16 @@ const SqlObjectExplorer = ({
|
|||
const buildDropSql = (node: TreeNode): string => {
|
||||
if (node.folder === 'tables') {
|
||||
const t = node.data as DatabaseTableDto
|
||||
return `DROP TABLE ${t.fullName ?? `[${t.schemaName}].[${t.tableName}]`};`
|
||||
const fullName = t.fullName ?? (isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
|
||||
return isPostgreSql ? `DROP TABLE IF EXISTS ${fullName};` : `DROP TABLE ${fullName};`
|
||||
}
|
||||
const obj = node.data as SqlNativeObjectDto
|
||||
const fullName = obj.fullName ?? (isPostgreSql ? `"${obj.schemaName}"."${obj.objectName}"` : `[${obj.schemaName}].[${obj.objectName}]`)
|
||||
const keyword =
|
||||
node.folder === 'views' ? 'VIEW' :
|
||||
node.folder === 'procedures' ? 'PROCEDURE' :
|
||||
'FUNCTION'
|
||||
return `DROP ${keyword} ${obj.fullName ?? `[${obj.schemaName}].[${obj. objectName}]`};`
|
||||
return isPostgreSql ? `DROP ${keyword} IF EXISTS ${fullName};` : `DROP ${keyword} ${fullName};`
|
||||
}
|
||||
|
||||
const getSqlDataFileCandidates = (node: TreeNode): string[] => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Container from '@/components/shared/Container'
|
|||
import ConfirmDialog from '@/components/shared/ConfirmDialog'
|
||||
import { getDataSources } from '@/services/data-source.service'
|
||||
import type { DataSourceDto } from '@/proxy/data-source'
|
||||
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
||||
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa'
|
||||
|
|
@ -139,9 +140,16 @@ const SqlQueryManager = () => {
|
|||
const escapeSqlLiteral = (value: string) => value.replace(/'/g, "''")
|
||||
|
||||
const escapeSqlIdentifier = (value: string) => value.replace(/]/g, ']]')
|
||||
const escapePgIdentifier = (value: string) => value.replace(/"/g, '""')
|
||||
|
||||
const getSafeFullName = (schemaName: string, objectName: string) =>
|
||||
`[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]`
|
||||
const getSafePgFullName = (schemaName: string, objectName: string) =>
|
||||
`"${escapePgIdentifier(schemaName)}"."${escapePgIdentifier(objectName)}"`
|
||||
const selectedDataSourceType = state.dataSources.find(
|
||||
(item) => item.code === state.selectedDataSource,
|
||||
)?.dataSourceType
|
||||
const isPostgreSql = selectedDataSourceType === DataSourceTypeEnum.Postgresql
|
||||
|
||||
const buildTableScriptQuery = (schemaName: string, tableName: string) => {
|
||||
const fullName = getSafeFullName(schemaName, tableName)
|
||||
|
|
@ -231,23 +239,40 @@ SELECT
|
|||
const getTableCreateScript = async (schemaName: string, tableName: string): Promise<string> => {
|
||||
if (!state.selectedDataSource) return ''
|
||||
|
||||
const result = await sqlObjectManagerService.executeQuery({
|
||||
queryText: buildTableScriptQuery(schemaName, tableName),
|
||||
dataSourceCode: state.selectedDataSource,
|
||||
})
|
||||
const result = await sqlObjectManagerService.getTableCreateScript(
|
||||
state.selectedDataSource,
|
||||
schemaName,
|
||||
tableName,
|
||||
)
|
||||
|
||||
const firstRow = result.data?.data?.[0]
|
||||
if (!firstRow) return ''
|
||||
|
||||
return firstRow.Script || firstRow.script || ''
|
||||
return result.data || ''
|
||||
}
|
||||
|
||||
const normalizeNativeDefinitionToCreate = (definition: string) => {
|
||||
if (!definition?.trim()) return ''
|
||||
if (isPostgreSql) return definition
|
||||
return definition.replace(/^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?/i, 'CREATE OR ALTER ')
|
||||
}
|
||||
|
||||
const buildDropIfExistsScript = (obj: SqlExplorerSelectedObject) => {
|
||||
if (isPostgreSql) {
|
||||
const fullName = getSafePgFullName(obj.schemaName, obj.objectName)
|
||||
|
||||
if (obj.objectType === 'table') {
|
||||
return `DROP TABLE IF EXISTS ${fullName};`
|
||||
}
|
||||
|
||||
if (obj.objectType === 'view') {
|
||||
return `DROP VIEW IF EXISTS ${fullName};`
|
||||
}
|
||||
|
||||
if (obj.objectType === 'procedure') {
|
||||
return `DROP PROCEDURE IF EXISTS ${fullName};`
|
||||
}
|
||||
|
||||
return `DROP FUNCTION IF EXISTS ${fullName};`
|
||||
}
|
||||
|
||||
const fullName = getSafeFullName(obj.schemaName, obj.objectName)
|
||||
|
||||
if (obj.objectType === 'table') {
|
||||
|
|
@ -266,6 +291,33 @@ SELECT
|
|||
}
|
||||
|
||||
const buildObjectExistsCheckQuery = (obj: SqlExplorerSelectedObject) => {
|
||||
if (isPostgreSql) {
|
||||
const schema = escapeSqlLiteral(obj.schemaName)
|
||||
const name = escapeSqlLiteral(obj.objectName)
|
||||
|
||||
if (obj.objectType === 'table') {
|
||||
return `SELECT CASE WHEN EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = '${schema}' AND table_name = '${name}' AND table_type = 'BASE TABLE'
|
||||
) THEN 1 ELSE 0 END AS "ExistsFlag";`
|
||||
}
|
||||
|
||||
if (obj.objectType === 'view') {
|
||||
return `SELECT CASE WHEN EXISTS (
|
||||
SELECT 1 FROM information_schema.views
|
||||
WHERE table_schema = '${schema}' AND table_name = '${name}'
|
||||
) THEN 1 ELSE 0 END AS "ExistsFlag";`
|
||||
}
|
||||
|
||||
const proKind = obj.objectType === 'procedure' ? 'p' : 'f'
|
||||
return `SELECT CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_proc p
|
||||
INNER JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE n.nspname = '${schema}' AND p.proname = '${name}' AND p.prokind = '${proKind}'
|
||||
) THEN 1 ELSE 0 END AS "ExistsFlag";`
|
||||
}
|
||||
|
||||
const fullName = getSafeFullName(obj.schemaName, obj.objectName)
|
||||
const escapedFullName = escapeSqlLiteral(fullName)
|
||||
|
||||
|
|
@ -299,6 +351,88 @@ SELECT
|
|||
}
|
||||
|
||||
const getTemplateContent = (templateType: string): string => {
|
||||
if (isPostgreSql) {
|
||||
const pgTemplates: Record<string, string> = {
|
||||
select: `-- Basic SELECT query
|
||||
SELECT
|
||||
"Column1",
|
||||
"Column2",
|
||||
"Column3"
|
||||
FROM
|
||||
"public"."TableName"
|
||||
WHERE
|
||||
"Column1" = 'value'
|
||||
ORDER BY
|
||||
"Column1" ASC
|
||||
LIMIT 100;`,
|
||||
|
||||
insert: `-- Basic INSERT query
|
||||
INSERT INTO "public"."TableName" ("Column1", "Column2", "Column3")
|
||||
VALUES
|
||||
('Value1', 'Value2', 'Value3');`,
|
||||
|
||||
update: `-- Basic UPDATE query
|
||||
UPDATE "public"."TableName"
|
||||
SET
|
||||
"Column1" = 'NewValue1',
|
||||
"Column2" = 'NewValue2'
|
||||
WHERE
|
||||
"Id" = '00000000-0000-0000-0000-000000000000';`,
|
||||
|
||||
delete: `-- Basic DELETE query
|
||||
DELETE FROM "public"."TableName"
|
||||
WHERE
|
||||
"Id" = '00000000-0000-0000-0000-000000000000';`,
|
||||
|
||||
'create-procedure': `-- Create Stored Procedure
|
||||
CREATE OR REPLACE PROCEDURE "public"."ProcedureName"(
|
||||
"Parameter1" integer,
|
||||
"Parameter2" varchar
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Add your logic here
|
||||
END;
|
||||
$$;`,
|
||||
|
||||
'create-view': `-- Create View
|
||||
CREATE OR REPLACE VIEW "public"."ViewName" AS
|
||||
SELECT
|
||||
t1."Column1",
|
||||
t1."Column2"
|
||||
FROM "public"."TableName1" t1
|
||||
WHERE t1."IsActive" = TRUE;`,
|
||||
|
||||
'create-scalar-function': `-- Create Scalar Function
|
||||
CREATE OR REPLACE FUNCTION "public"."ScalarFunctionName"(
|
||||
"Parameter1" integer,
|
||||
"Parameter2" varchar
|
||||
)
|
||||
RETURNS varchar
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN "Parameter2";
|
||||
END;
|
||||
$$;`,
|
||||
|
||||
'create-table-function': `-- Create Table-Valued Function
|
||||
CREATE OR REPLACE FUNCTION "public"."TableFunctionName"(
|
||||
"Parameter1" integer
|
||||
)
|
||||
RETURNS TABLE("Column1" integer, "Column2" varchar)
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT t."Column1", t."Column2"
|
||||
FROM "public"."TableName" t
|
||||
WHERE t."Id" = "Parameter1";
|
||||
$$;`,
|
||||
}
|
||||
|
||||
return pgTemplates[templateType] || pgTemplates.select
|
||||
}
|
||||
|
||||
const templates: Record<string, string> = {
|
||||
select: `-- Basic SELECT query
|
||||
SELECT
|
||||
|
|
@ -981,6 +1115,7 @@ GO`,
|
|||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||
<SqlObjectExplorer
|
||||
dataSource={state.selectedDataSource}
|
||||
dataSourceType={selectedDataSourceType}
|
||||
onTemplateSelect={handleTemplateSelect}
|
||||
onViewDefinition={handleViewDefinition}
|
||||
onGenerateTableScript={handleGenerateTableScript}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
const [hoveredCommentAuthor, setHoveredCommentAuthor] = useState<string | null>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const { user } = useStoreState((state) => state.auth)
|
||||
const postUser = post.user ?? user ?? {}
|
||||
const postComments = post.comments ?? []
|
||||
const postLikeCount = post.likeCount ?? 0
|
||||
const postUserId = postUser.id ?? ''
|
||||
const postUserTenantId = postUser.tenantId ?? ''
|
||||
const postUserFullName =
|
||||
postUser.fullName || [postUser.name, postUser.surname].filter(Boolean).join(' ') || '-'
|
||||
const postUserTitle = postUser.jobPositions?.[0]?.name || ''
|
||||
|
||||
// Intersection Observer for video autoplay/pause
|
||||
useEffect(() => {
|
||||
|
|
@ -177,7 +185,8 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
|
||||
case 'poll':
|
||||
if (post.media.pollQuestion && post.media.pollOptions) {
|
||||
const isExpired = post.media.pollEndsAt ? new Date() > post.media.pollEndsAt : false
|
||||
const pollEndsAt = post.media.pollEndsAt ? new Date(post.media.pollEndsAt) : null
|
||||
const isExpired = pollEndsAt ? new Date() > pollEndsAt : false
|
||||
const hasVoted = !!post.media.pollUserVoteId
|
||||
const totalVotes = post.media.pollTotalVotes || 0
|
||||
const pollUserVoteId = post.media.pollUserVoteId
|
||||
|
|
@ -189,13 +198,13 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
</h4>
|
||||
<div className="space-y-2">
|
||||
{post.media.pollOptions.map((option) => {
|
||||
const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0
|
||||
const percentage = totalVotes > 0 ? ((option.votes ?? 0) / totalVotes) * 100 : 0
|
||||
const isSelected = pollUserVoteId === option.id
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
onClick={() => !hasVoted && !isExpired && onVote(post.id, option.id)}
|
||||
onClick={() => option.id && !hasVoted && !isExpired && onVote(post.id, option.id)}
|
||||
disabled={hasVoted || isExpired}
|
||||
className={classNames(
|
||||
'w-full text-left p-3 rounded-lg relative overflow-hidden transition-all',
|
||||
|
|
@ -231,8 +240,8 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
{totalVotes} oy •{' '}
|
||||
{isExpired
|
||||
? 'Sona erdi'
|
||||
: post.media.pollEndsAt
|
||||
? dayjs(post.media.pollEndsAt).fromNow() + ' bitiyor'
|
||||
: pollEndsAt
|
||||
? dayjs(pollEndsAt).fromNow() + ' bitiyor'
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -259,18 +268,18 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
onMouseEnter={() => setShowUserCard(true)}
|
||||
onMouseLeave={() => setShowUserCard(false)}
|
||||
>
|
||||
<Avatar size={32} shape="circle" src={AVATAR_URL(post.user.id, post.user.tenantId)} />
|
||||
<Avatar size={32} shape="circle" src={AVATAR_URL(postUserId, postUserTenantId)} />
|
||||
<AnimatePresence>
|
||||
{showUserCard && (
|
||||
<UserProfileCard
|
||||
user={{
|
||||
id: post.user.id || '',
|
||||
name: post.user.fullName || '',
|
||||
title: post.user.jobPositions?.[0]?.name || '',
|
||||
email: post.user.email,
|
||||
phoneNumber: post.user.phoneNumber,
|
||||
department: post.user.departments?.[0]?.name,
|
||||
tenantId: post.user.tenantId || '',
|
||||
id: postUserId,
|
||||
name: postUserFullName,
|
||||
title: postUserTitle,
|
||||
email: postUser.email,
|
||||
phoneNumber: postUser.phoneNumber,
|
||||
department: postUser.departments?.[0]?.name,
|
||||
tenantId: postUserTenantId,
|
||||
}}
|
||||
position="bottom"
|
||||
/>
|
||||
|
|
@ -278,9 +287,9 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
</AnimatePresence>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm text-gray-900 dark:text-gray-100">{post.user.fullName}</h3>
|
||||
<h3 className="text-sm text-gray-900 dark:text-gray-100">{postUserFullName}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{post.user.jobPositions?.[0]?.name || ''} • {dayjs(post.creationTime).fromNow()}
|
||||
{postUserTitle} • {dayjs(post.creationTime).fromNow()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -313,7 +322,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
)}
|
||||
>
|
||||
{post.isLiked ? <FaHeart className="w-5 h-5" /> : <FaRegHeart className="w-5 h-5" />}
|
||||
<span className="text-sm font-medium">{post.likeCount}</span>
|
||||
<span className="text-sm font-medium">{postLikeCount}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -321,7 +330,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<FaRegCommentAlt className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{post.comments.length}</span>
|
||||
<span className="text-sm font-medium">{postComments.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -358,7 +367,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
|
||||
{/* Comments List */}
|
||||
<div className="space-y-3">
|
||||
{post.comments.map((comment) => (
|
||||
{postComments.map((comment) => (
|
||||
<div key={comment.id} className="flex gap-3">
|
||||
<div
|
||||
className="relative"
|
||||
|
|
@ -368,16 +377,16 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
<Avatar
|
||||
size={32}
|
||||
shape="circle"
|
||||
src={AVATAR_URL(comment.user.id, comment.user.tenantId)}
|
||||
src={AVATAR_URL(comment.user?.id ?? '', comment.user?.tenantId ?? '')}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{hoveredCommentAuthor === comment.id && (
|
||||
<UserProfileCard
|
||||
user={{
|
||||
id: comment.user.id || '',
|
||||
name: comment.user.fullName || '',
|
||||
title: comment.user.jobPositions?.[0]?.name || '',
|
||||
tenantId: comment.user.tenantId || '',
|
||||
id: comment.user?.id || '',
|
||||
name: comment.user?.fullName || '',
|
||||
title: comment.user?.jobPositions?.[0]?.name || '',
|
||||
tenantId: comment.user?.tenantId || '',
|
||||
}}
|
||||
position="bottom"
|
||||
/>
|
||||
|
|
@ -387,7 +396,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
<div className="flex-1">
|
||||
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
|
||||
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
||||
{comment.user.fullName}
|
||||
{comment.user?.fullName || '-'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,14 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
intranetService.incrementAnnouncementViewCount(announcement.id)
|
||||
}, [announcement.id])
|
||||
|
||||
const announcementUser = announcement.user ?? {}
|
||||
const announcementUserName =
|
||||
announcementUser.fullName ||
|
||||
[announcementUser.name, announcementUser.surname].filter(Boolean).join(' ') ||
|
||||
'-'
|
||||
const images = announcement.imageUrl ? announcement.imageUrl.split('|').filter(Boolean) : []
|
||||
const attachments = Array.isArray(announcement.attachments) ? announcement.attachments : []
|
||||
const category = announcement.category || 'general'
|
||||
|
||||
const imgSrc = (img: string) =>
|
||||
img.startsWith('data:') || img.startsWith('http://') || img.startsWith('https://') || img.startsWith('/')
|
||||
|
|
@ -79,17 +86,17 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full ${getCategoryColor(announcement.category)}`}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full ${getCategoryColor(category)}`}
|
||||
>
|
||||
{announcement.category === 'general' &&
|
||||
{category === 'general' &&
|
||||
`📢 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.General')}`}
|
||||
{announcement.category === 'hr' &&
|
||||
{category === 'hr' &&
|
||||
`👥 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.HR')}`}
|
||||
{announcement.category === 'it' &&
|
||||
{category === 'it' &&
|
||||
`💻 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.IT')}`}
|
||||
{announcement.category === 'event' &&
|
||||
{category === 'event' &&
|
||||
`🎉 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Event')}`}
|
||||
{announcement.category === 'urgent' &&
|
||||
{category === 'urgent' &&
|
||||
`🚨 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Urgent')}`}
|
||||
</span>
|
||||
{announcement.isPinned && (
|
||||
|
|
@ -115,11 +122,11 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
<Avatar
|
||||
size={32}
|
||||
shape="circle"
|
||||
src={AVATAR_URL(announcement.user.id, announcement.user.tenantId)}
|
||||
src={AVATAR_URL(announcementUser.id ?? '', announcementUser.tenantId ?? '')}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">
|
||||
{announcement.user.fullName}
|
||||
{announcementUserName}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>{currentLocalDate(announcement.publishDate, currentLocale || 'tr')}</span>
|
||||
|
|
@ -174,15 +181,15 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
|
|||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{announcement.attachments && announcement.attachments.length > 0 && (
|
||||
{attachments.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<FaClipboard className="w-5 h-5" />
|
||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Attachments')} (
|
||||
{announcement.attachments.length})
|
||||
{attachments.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{announcement.attachments.map((attachment, idx) => (
|
||||
{attachments.map((attachment, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={attachment.url}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ interface AnnouncementsProps {
|
|||
}
|
||||
|
||||
const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnouncementClick }) => {
|
||||
const pinnedAnnouncements = announcements.filter((a) => a.isPinned).slice(0, 3)
|
||||
const safeAnnouncements = announcements ?? []
|
||||
const pinnedAnnouncements = safeAnnouncements.filter((a) => a?.isPinned).slice(0, 3)
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
|
|
@ -26,6 +27,15 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
|
|||
return colors[category] || colors.general
|
||||
}
|
||||
|
||||
const getUserName = (announcement: AnnouncementDto) => {
|
||||
const announcementUser = announcement.user
|
||||
return (
|
||||
announcementUser?.fullName ||
|
||||
[announcementUser?.name, announcementUser?.surname].filter(Boolean).join(' ') ||
|
||||
'-'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
|
|
@ -47,11 +57,11 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
|
|||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{announcement.title}
|
||||
{announcement.title || '-'}
|
||||
</h3>
|
||||
{announcement.category && (
|
||||
<span
|
||||
className={`px-2 py-1 text-center text-xs rounded-full ${getCategoryColor(announcement.category)}`}
|
||||
className={`px-2 py-1 text-center text-xs rounded-full ${getCategoryColor(announcement.category || 'general')}`}
|
||||
>
|
||||
{(() => {
|
||||
const key = `::App.Platform.Intranet.Widgets.Announcements.Category.${announcement.category.charAt(0).toUpperCase() + announcement.category.slice(1)}`
|
||||
|
|
@ -62,21 +72,21 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
|
|||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{announcement.excerpt}
|
||||
{announcement.excerpt || ''}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<Avatar
|
||||
size={24}
|
||||
shape="circle"
|
||||
src={AVATAR_URL(announcement.user.id, announcement.user.tenantId)}
|
||||
src={AVATAR_URL(announcement.user?.id ?? '', announcement.user?.tenantId ?? '')}
|
||||
/>
|
||||
<span>{announcement.user.fullName}</span>
|
||||
<span>{getUserName(announcement)}</span>
|
||||
<span>•</span>
|
||||
<span>{dayjs(announcement.publishDate).fromNow()}</span>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FaEye className="w-3 h-3" />
|
||||
{announcement.viewCount}
|
||||
{announcement.viewCount ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,10 +35,16 @@ const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick })
|
|||
const { translate } = useLocalization()
|
||||
|
||||
const now = dayjs()
|
||||
const upcomingEvents = events
|
||||
.filter((event) => event.isPublished && !dayjs(event.date).isBefore(now, 'day'))
|
||||
const upcomingEvents = (events ?? [])
|
||||
.filter((event) => event?.isPublished && !dayjs(event.date).isBefore(now, 'day'))
|
||||
.sort((left, right) => dayjs(left.date).valueOf() - dayjs(right.date).valueOf())
|
||||
|
||||
const getUserName = (event: EventDto) => (
|
||||
event.user?.fullName ||
|
||||
[event.user?.name, event.user?.surname].filter(Boolean).join(' ') ||
|
||||
'-'
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
|
|
@ -72,9 +78,9 @@ const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick })
|
|||
<Avatar
|
||||
size={24}
|
||||
shape="circle"
|
||||
src={AVATAR_URL(event.user.id, event.user.tenantId)}
|
||||
src={AVATAR_URL(event.user?.id ?? '', event.user?.tenantId ?? '')}
|
||||
/>
|
||||
<span>{event.user.fullName}</span>
|
||||
<span>{getUserName(event)}</span>
|
||||
<span>•</span>
|
||||
<span>{dayjs(event.date).fromNow()}</span>
|
||||
{event.likes > 0 && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue