PostgreSql üzerinde çalışması için çalışma

This commit is contained in:
Sedat Öztürk 2026-05-25 17:31:54 +03:00
parent 0b5eb3d978
commit 01e19ef26f
47 changed files with 10764 additions and 8303 deletions

View file

@ -15,7 +15,7 @@ public static class SettingsConsts
public const string FormNamePrefix = "Setting_"; public const string FormNamePrefix = "Setting_";
public const string DefaultDatabaseProvider = DatabaseProvider.SqlServer; public const string DefaultDatabaseProvider = DatabaseProvider.PostgreSql;
public static class DatabaseProvider public static class DatabaseProvider
{ {

View file

@ -4,7 +4,10 @@ public class DatabaseTableDto
{ {
public string SchemaName { get; set; } public string SchemaName { get; set; }
public string TableName { 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 public class DatabaseColumnDto

View file

@ -22,6 +22,7 @@ public interface ISqlObjectManagerAppService : IApplicationService
// Database Metadata Operations // Database Metadata Operations
Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName); Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName);
Task<string> GetTableCreateScriptAsync(string dataSourceCode, string schemaName, string tableName);
/// <summary> /// <summary>
/// Gets the SQL definition/body of a native SQL Server object (Stored Procedure, View, or Function) /// Gets the SQL definition/body of a native SQL Server object (Stored Procedure, View, or Function)

View file

@ -9,7 +9,10 @@ public class SqlNativeObjectDto
{ {
public string SchemaName { get; set; } = "dbo"; public string SchemaName { get; set; } = "dbo";
public string ObjectName { get; set; } = ""; 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> /// <summary>

View file

@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Queries;
namespace Sozsoft.SqlQueryManager.Application; namespace Sozsoft.SqlQueryManager.Application;
@ -43,6 +45,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
private readonly ISqlExecutorService _sqlExecutorService; private readonly ISqlExecutorService _sqlExecutorService;
private readonly ISqlTemplateProvider _templateProvider; private readonly ISqlTemplateProvider _templateProvider;
private readonly IDataSourceManager _dataSourceManager;
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHostEnvironment _hostEnvironment; private readonly IHostEnvironment _hostEnvironment;
@ -51,6 +54,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public SqlObjectManagerAppService( public SqlObjectManagerAppService(
ISqlExecutorService sqlExecutorService, ISqlExecutorService sqlExecutorService,
ISqlTemplateProvider templateProvider, ISqlTemplateProvider templateProvider,
IDataSourceManager dataSourceManager,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
IHostEnvironment hostEnvironment, IHostEnvironment hostEnvironment,
@ -58,6 +62,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
_sqlExecutorService = sqlExecutorService; _sqlExecutorService = sqlExecutorService;
_templateProvider = templateProvider; _templateProvider = templateProvider;
_dataSourceManager = dataSourceManager;
_currentTenant = currentTenant; _currentTenant = currentTenant;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_hostEnvironment = hostEnvironment; _hostEnvironment = hostEnvironment;
@ -90,11 +95,12 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
ValidateTenantAccess(); ValidateTenantAccess();
var result = new SqlObjectExplorerDto(); var result = new SqlObjectExplorerDto();
var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
result.Tables = await GetTablesAsync(dataSourceCode); result.Tables = await GetTablesAsync(dataSourceCode, dataSourceType);
result.Views = await GetNativeObjectsAsync(dataSourceCode, "V"); result.Views = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "V");
result.StoredProcedures = await GetNativeObjectsAsync(dataSourceCode, "P"); result.StoredProcedures = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "P");
result.Functions = await GetNativeObjectsAsync(dataSourceCode, "FN", "IF", "TF"); result.Functions = await GetNativeObjectsAsync(dataSourceCode, dataSourceType, "FN", "IF", "TF");
result.Templates = _templateProvider.GetAvailableQueryTemplates() result.Templates = _templateProvider.GetAvailableQueryTemplates()
.Select(t => new SqlTemplateDto .Select(t => new SqlTemplateDto
@ -109,20 +115,25 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return result; 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 dataSource = await _dataSourceManager.GetDataSourceAsync(_currentTenant.IsAvailable, dataSourceCode);
var query = $@" if (dataSource == null)
SELECT {
SCHEMA_NAME(o.schema_id) AS SchemaName, throw new Volo.Abp.UserFriendlyException($"Data source '{dataSourceCode}' was not found.");
o.name AS ObjectName }
FROM
sys.objects o return dataSource.DataSourceType;
WHERE }
o.type IN ({typeList})
AND o.is_ms_shipped = 0 private async Task<List<SqlNativeObjectDto>> GetNativeObjectsAsync(
ORDER BY string dataSourceCode,
SCHEMA_NAME(o.schema_id), o.name"; DataSourceTypeEnum dataSourceType,
params string[] objectTypes)
{
var query = dataSourceType == DataSourceTypeEnum.Postgresql
? BuildPostgreSqlNativeObjectsQuery(objectTypes)
: BuildSqlServerNativeObjectsQuery(objectTypes);
var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode);
@ -136,8 +147,9 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
objects.Add(new SqlNativeObjectDto objects.Add(new SqlNativeObjectDto
{ {
SchemaName = dict["SchemaName"]?.ToString() ?? "dbo", SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? GetDefaultSchemaName(dataSourceType),
ObjectName = dict["ObjectName"]?.ToString() ?? "" ObjectName = GetDictionaryValue(dict, "ObjectName")?.ToString() ?? "",
DataSourceType = dataSourceType.ToString()
}); });
} }
} }
@ -146,9 +158,73 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return objects; 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 SELECT
SCHEMA_NAME(t.schema_id) AS SchemaName, SCHEMA_NAME(t.schema_id) AS SchemaName,
t.name AS TableName t.name AS TableName
@ -171,8 +247,9 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
tables.Add(new DatabaseTableDto tables.Add(new DatabaseTableDto
{ {
SchemaName = dict["SchemaName"]?.ToString() ?? "dbo", SchemaName = GetDictionaryValue(dict, "SchemaName")?.ToString() ?? GetDefaultSchemaName(dataSourceType),
TableName = dict["TableName"]?.ToString() ?? "" 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) public async Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName)
{ {
ValidateTenantAccess(); ValidateTenantAccess();
var query = @" var dataSourceType = await GetDataSourceTypeAsync(dataSourceCode);
SELECT OBJECT_DEFINITION(OBJECT_ID(@ObjectName)) AS Definition";
var fullObjectName = $"[{schemaName}].[{objectName}]";
var result = await _sqlExecutorService.ExecuteQueryAsync( var result = await _sqlExecutorService.ExecuteQueryAsync(
query.Replace("@ObjectName", $"'{fullObjectName}'"), BuildNativeObjectDefinitionQuery(dataSourceType, schemaName, objectName),
dataSourceCode); dataSourceCode);
if (result.Success && result.Data != null) if (result.Success && result.Data != null)
@ -243,15 +317,18 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
if (dataList.Count > 0) if (dataList.Count > 0)
{ {
var row = dataList[0] as IDictionary<string, object>; 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. // Always open object script as CREATE OR ALTER in editor.
definition = Regex.Replace( if (dataSourceType == DataSourceTypeEnum.Mssql)
definition, {
@"^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?", definition = Regex.Replace(
"CREATE OR ALTER ", definition,
RegexOptions.IgnoreCase); @"^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?",
"CREATE OR ALTER ",
RegexOptions.IgnoreCase);
}
return definition; return definition;
} }
} }
@ -260,10 +337,59 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return string.Empty; 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) public async Task<List<DatabaseColumnDto>> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName)
{ {
ValidateTenantAccess(); 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 SELECT
c.name AS ColumnName, c.name AS ColumnName,
TYPE_NAME(c.user_type_id) AS DataType, 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.tables t ON c.object_id = t.object_id
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
WHERE WHERE
s.name = '{schemaName}' s.name = {ToSqlLiteral(schemaName)}
AND t.name = '{tableName}' AND t.name = {ToSqlLiteral(tableName)}
ORDER BY ORDER BY
c.column_id"; c.column_id";
@ -291,10 +417,10 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
{ {
columns.Add(new DatabaseColumnDto columns.Add(new DatabaseColumnDto
{ {
ColumnName = dict["ColumnName"]?.ToString() ?? "", ColumnName = GetDictionaryValue(dict, "ColumnName")?.ToString() ?? "",
DataType = dict["DataType"]?.ToString() ?? "", DataType = GetDictionaryValue(dict, "DataType")?.ToString() ?? "",
IsNullable = dict["IsNullable"] is bool b && b, IsNullable = ToBoolean(GetDictionaryValue(dict, "IsNullable")),
MaxLength = dict["MaxLength"] != null ? int.Parse(dict["MaxLength"].ToString()) : null MaxLength = ToNullableInt(GetDictionaryValue(dict, "MaxLength"))
}); });
} }
} }
@ -303,6 +429,218 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return columns; 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) private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result, bool isDeployed = false)
{ {
return new SqlQueryExecutionResultDto return new SqlQueryExecutionResultDto

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Queries; using Sozsoft.Platform.Queries;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp; using Volo.Abp;
@ -38,14 +39,25 @@ public class SqlExecutorService : DomainService, ISqlExecutorService
.WithData("DataSourceCode", dataSourceCode); .WithData("DataSourceCode", dataSourceCode);
} }
// Get appropriate repository based on database type var repositoryKey = dataSource.DataSourceType switch
// For now, using MS SQL Server repository {
var repository = _serviceProvider.GetKeyedService<IDynamicDataRepository>("Ms"); 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) if (repository == null)
{ {
throw new BusinessException("SqlQueryManager:RepositoryNotFound") throw new BusinessException("SqlQueryManager:RepositoryNotFound")
.WithData("DatabaseType", "Ms"); .WithData("DatabaseType", repositoryKey);
} }
return repository; return repository;

View file

@ -8,16 +8,16 @@ public class AnnouncementDto : FullAuditedEntityDto<Guid>
{ {
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public string Title { get; set; } public string Title { get; set; } = string.Empty;
public string Excerpt { get; set; } public string Excerpt { get; set; } = string.Empty;
public string Content { get; set; } public string Content { get; set; } = string.Empty;
public string ImageUrl { get; set; } public string ImageUrl { get; set; } = string.Empty;
public string Category { get; set; } public string Category { get; set; } = string.Empty;
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public UserInfoViewModel User { get; set; } public UserInfoViewModel? User { get; set; }
public DateTime PublishDate { get; set; } public DateTime PublishDate { get; set; }
public DateTime? ExpiryDate { get; set; } public DateTime? ExpiryDate { get; set; }
public bool IsPinned { get; set; } public bool IsPinned { get; set; }
public int ViewCount { get; set; } public int ViewCount { get; set; }
public string Attachments { get; set; } public string Attachments { get; set; } = string.Empty;
} }

View file

@ -9,7 +9,7 @@ public class SocialPostDto : FullAuditedEntityDto<Guid>
{ {
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public UserInfoViewModel? User { 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 int LikeCount { get; set; }
public bool IsLiked { get; set; } public bool IsLiked { get; set; }
@ -17,14 +17,14 @@ public class SocialPostDto : FullAuditedEntityDto<Guid>
public SocialLocationDto? Location { get; set; } public SocialLocationDto? Location { get; set; }
public SocialMediaDto? Media { get; set; } public SocialMediaDto? Media { get; set; }
public List<SocialCommentDto> Comments { get; set; } public List<SocialCommentDto> Comments { get; set; } = [];
public List<SocialLikeDto> Likes { get; set; } public List<SocialLikeDto> Likes { get; set; } = [];
} }
public class SocialLocationDto : FullAuditedEntityDto<Guid> public class SocialLocationDto : FullAuditedEntityDto<Guid>
{ {
public Guid SocialPostId { get; set; } public Guid SocialPostId { get; set; }
public string Name { get; set; } public string Name { get; set; } = string.Empty;
public string? Address { get; set; } public string? Address { get; set; }
public double? Lat { get; set; } public double? Lat { get; set; }
public double? Lng { get; set; } public double? Lng { get; set; }
@ -34,8 +34,8 @@ public class SocialLocationDto : FullAuditedEntityDto<Guid>
public class SocialMediaDto : FullAuditedEntityDto<Guid> public class SocialMediaDto : FullAuditedEntityDto<Guid>
{ {
public Guid SocialPostId { get; set; } public Guid SocialPostId { get; set; }
public string Type { get; set; } // image | video | poll public string Type { get; set; } = string.Empty; // image | video | poll
public string[] Urls { get; set; } public string[] Urls { get; set; } = [];
// Poll Fields // Poll Fields
public string? PollQuestion { get; set; } public string? PollQuestion { get; set; }
@ -43,13 +43,13 @@ public class SocialMediaDto : FullAuditedEntityDto<Guid>
public DateTime? PollEndsAt { get; set; } public DateTime? PollEndsAt { get; set; }
public string? PollUserVoteId { 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 class SocialPollOptionDto : FullAuditedEntityDto<Guid>
{ {
public Guid SocialMediaId { get; set; } public Guid SocialMediaId { get; set; }
public string Text { get; set; } public string Text { get; set; } = string.Empty;
public int Votes { get; set; } public int Votes { get; set; }
} }
@ -58,7 +58,7 @@ public class SocialCommentDto : FullAuditedEntityDto<Guid>
public Guid SocialPostId { get; set; } public Guid SocialPostId { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public UserInfoViewModel? User { get; set; } public UserInfoViewModel? User { get; set; }
public string Content { get; set; } public string Content { get; set; } = string.Empty;
} }
public class SocialLikeDto : FullAuditedEntityDto<Guid> public class SocialLikeDto : FullAuditedEntityDto<Guid>

View file

@ -123,6 +123,22 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
.MapDepartmentAndJobPositionAssignments(departmentDict, jobPositionDict); .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() private async Task<List<EventDto>> GetUpcomingEventsAsync()
{ {
var queryable = await _eventRepository var queryable = await _eventRepository
@ -166,6 +182,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
} }
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict);
var users = await _identityUserRepository.GetListAsync(); var users = await _identityUserRepository.GetListAsync();
var userDict = users var userDict = users
@ -178,7 +195,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
var result = new List<EventDto>(); var result = new List<EventDto>();
foreach (var evt in events) 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; continue;
var commentDtos = new List<EventCommentDto>(); var commentDtos = new List<EventCommentDto>();
@ -346,17 +367,22 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
var announcementDtos = new List<AnnouncementDto>(); var announcementDtos = new List<AnnouncementDto>();
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict);
foreach (var announcement in announcements) foreach (var announcement in announcements)
{ {
var dto = ObjectMapper.Map<Announcement, AnnouncementDto>(announcement); var dto = ObjectMapper.Map<Announcement, AnnouncementDto>(announcement);
var user = await _identityUserRepository.FindAsync(announcement.UserId ?? Guid.Empty); if (announcement.UserId.HasValue)
if (user != null)
{ {
dto.User = MapUserInfoViewModel(user, departmentDict, jobPositionDict); var user = await _identityUserRepository.FindAsync(announcement.UserId.Value);
if (user != null)
{
dto.User = MapUserInfoViewModel(user, departmentDict, jobPositionDict);
}
} }
dto.User ??= fallbackUser;
announcementDtos.Add(dto); announcementDtos.Add(dto);
} }
@ -433,8 +459,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
// Collect all unique user IDs to resolve in a single query // Collect all unique user IDs to resolve in a single query
var userIds = dtos var userIds = dtos
.Select(p => p.UserId) .Select(p => p.UserId)
.Union(dtos.SelectMany(p => p.Comments.Select(c => c.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.Likes ?? []).Select(l => l.UserId)))
.Where(id => id.HasValue) .Where(id => id.HasValue)
.Select(id => id!.Value) .Select(id => id!.Value)
.Distinct() .Distinct()
@ -454,11 +480,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
if (dto.UserId.HasValue && userMap.TryGetValue(dto.UserId.Value, out var postUser)) if (dto.UserId.HasValue && userMap.TryGetValue(dto.UserId.Value, out var postUser))
dto.User = 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)) if (comment.UserId.HasValue && userMap.TryGetValue(comment.UserId.Value, out var commentUser))
comment.User = 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)) if (like.UserId.HasValue && userMap.TryGetValue(like.UserId.Value, out var likeUser))
like.User = likeUser; like.User = likeUser;
} }
@ -467,7 +493,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
foreach (var dto in dtos) foreach (var dto in dtos)
{ {
dto.IsOwnPost = dto.UserId == CurrentUser.Id; 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); await EnrichPollOptionsAsync(dtos);

View file

@ -6,6 +6,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.DbMigrator; namespace Sozsoft.Platform.DbMigrator;
@ -13,6 +14,8 @@ class Program
{ {
static async Task Main(string[] args) static async Task Main(string[] args)
{ {
ConfigurePostgreSqlCompatibility();
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information() .MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
@ -30,6 +33,14 @@ class Program
await CreateHostBuilder(args).RunConsoleAsync(); await CreateHostBuilder(args).RunConsoleAsync();
} }
private static void ConfigurePostgreSqlCompatibility()
{
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
{
System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
}
}
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) Host.CreateDefaultBuilder(args)
.AddAppSettingsSecretsJson() .AddAppSettingsSecretsJson()

View file

@ -34,6 +34,12 @@ public class HangfireDbSchemaMigrator : IPlatformDbSchemaMigrator, ITransientDep
public async Task MigrateAsync() 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); var connectionString = _configuration.GetConnectionString(DefaultDatabaseProvider);
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(connectionString))
{ {

View file

@ -258,7 +258,7 @@
"code": "App.SiteManagement.General.NewMemberNotificationEmails", "code": "App.SiteManagement.General.NewMemberNotificationEmails",
"nameKey": "App.SiteManagement.General.NewMemberNotificationEmails", "nameKey": "App.SiteManagement.General.NewMemberNotificationEmails",
"descriptionKey": "App.SiteManagement.General.NewMemberNotificationEmails.Description", "descriptionKey": "App.SiteManagement.General.NewMemberNotificationEmails.Description",
"defaultValue": "system@sozsoft.com", "defaultValue": "SYSTEM@SOZSOFT.COM",
"isVisibleToClients": false, "isVisibleToClients": false,
"providers": "G|D", "providers": "G|D",
"isInherited": false, "isInherited": false,
@ -274,7 +274,7 @@
"code": "App.SiteManagement.General.TimedLoginEmails", "code": "App.SiteManagement.General.TimedLoginEmails",
"nameKey": "App.SiteManagement.General.TimedLoginEmails", "nameKey": "App.SiteManagement.General.TimedLoginEmails",
"descriptionKey": "App.SiteManagement.General.TimedLoginEmails.Description", "descriptionKey": "App.SiteManagement.General.TimedLoginEmails.Description",
"defaultValue": "system@sozsoft.com", "defaultValue": "SYSTEM@SOZSOFT.COM",
"isVisibleToClients": false, "isVisibleToClients": false,
"providers": "G|D", "providers": "G|D",
"isInherited": false, "isInherited": false,
@ -466,7 +466,7 @@
"code": "Abp.Mailing.DefaultFromAddress", "code": "Abp.Mailing.DefaultFromAddress",
"nameKey": "Abp.Mailing.DefaultFromAddress", "nameKey": "Abp.Mailing.DefaultFromAddress",
"descriptionKey": "Abp.Mailing.DefaultFromAddress.Description", "descriptionKey": "Abp.Mailing.DefaultFromAddress.Description",
"defaultValue": "system@sozsoft.com", "defaultValue": "SYSTEM@SOZSOFT.COM",
"isVisibleToClients": false, "isVisibleToClients": false,
"providers": "T|G|D", "providers": "T|G|D",
"isInherited": false, "isInherited": false,
@ -482,7 +482,7 @@
"code": "Abp.Mailing.Smtp.UserName", "code": "Abp.Mailing.Smtp.UserName",
"nameKey": "Abp.Mailing.Smtp.UserName", "nameKey": "Abp.Mailing.Smtp.UserName",
"descriptionKey": "Abp.Mailing.Smtp.UserName.Description", "descriptionKey": "Abp.Mailing.Smtp.UserName.Description",
"defaultValue": "system@sozsoft.com", "defaultValue": "SYSTEM@SOZSOFT.COM",
"isVisibleToClients": false, "isVisibleToClients": false,
"providers": "T|G|D", "providers": "T|G|D",
"isInherited": false, "isInherited": false,

View file

@ -212,6 +212,11 @@ public class HostDataSeeder : IDataSeedContributor, ITransientDependency
.ToListAsync()) .ToListAsync())
.ToHashSet(); .ToHashSet();
var countryGroupNames = (await dbCtx.Set<CountryGroup>()
.Select(c => c.Name)
.ToListAsync())
.ToHashSet();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
using var fs = File.OpenRead(Path.Combine("Seeds", "Countries.json")); 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)) if (!seenCodes.Add(item.Name) || existingCodes.Contains(item.Name))
continue; continue;
var groupName = string.IsNullOrWhiteSpace(item.GroupName)
? null
: item.GroupName.Trim();
if (!string.IsNullOrWhiteSpace(groupName) && !countryGroupNames.Contains(groupName))
{
groupName = null;
}
buffer.Add(new Country( buffer.Add(new Country(
item.Name, item.Name,
item.Name, item.Name,
item.GroupName, groupName,
item.Currency, item.Currency,
item.PhoneCode, item.PhoneCode,
item.TaxLabel item.TaxLabel

View file

@ -18440,6 +18440,12 @@
"en": "Failed", "en": "Failed",
"tr": "Başarısız" "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", "resourceName": "Platform",
"key": "App.DeveloperKit.DynamicServices.FailedDescription", "key": "App.DeveloperKit.DynamicServices.FailedDescription",

View file

@ -4164,11 +4164,11 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Event)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Event)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson, 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>() EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
{ {
new() { 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 = 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 }, 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 = 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 = 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 = 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 }, new EditingFormItemDto { Order = 10, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions },
]} ]}
}), }),

View file

@ -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;
$$;

View file

@ -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;
$$;

View file

@ -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;
$$;

View file

@ -9,6 +9,7 @@ using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds; namespace Sozsoft.Platform.Data.Seeds;
@ -32,10 +33,11 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
public async Task SeedAsync(DataSeedContext context) 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)) if (!Directory.Exists(sqlDataPath))
{ {
_logger.LogInformation("Seeds/SqlData directory not found, skipping SqlDataSeeder."); _logger.LogInformation("Seeds/{DirectoryName} directory not found, skipping SqlDataSeeder.", dataDirectoryName);
return; return;
} }
@ -45,11 +47,15 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
if (sqlFiles.Length == 0) 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; 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(); var dbContext = await _dbContextProvider.GetDbContextAsync();
@ -98,6 +104,13 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
_logger.LogInformation("SqlDataSeeder completed. {Count} file(s) processed.", sqlFiles.Length); _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) private static (string Action, string? ObjectName, string? ObjectType) ExtractSqlInfo(string sql)
{ {
var patterns = new[] var patterns = new[]

View file

@ -85,6 +85,10 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Seeds\PostgresData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -162,7 +162,7 @@ public static class PlatformConsts
public const string AdminRoleName = "admin"; public const string AdminRoleName = "admin";
public const string AdminNameDefaultValue = "Sedat"; public const string AdminNameDefaultValue = "Sedat";
public const string AdminSurNameDefaultValue = "ÖZTÜRK"; 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 AdminPasswordDefaultValue = "1q2w3E*";
public const string AdminPhoneNumberDefaultValue = "05449476346"; public const string AdminPhoneNumberDefaultValue = "05449476346";
public const string AdminRocketUsernameDefaultValue = "sedat.ozturk"; public const string AdminRocketUsernameDefaultValue = "sedat.ozturk";

View file

@ -21,47 +21,57 @@ public class DynamicEntityManager : IDynamicEntityManager
public async Task<List<object>?> GetEntityListAsync(string entityName) 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 hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var query = hasIsDeleted var query = hasIsDeleted
? $"SELECT * FROM [{entityName}] WHERE IsDeleted = 0 OR IsDeleted IS NULL" ? $"SELECT * FROM {tableName} WHERE {idDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {idDeletedColumn} IS NULL"
: $"SELECT * FROM [{entityName}]"; : $"SELECT * FROM {tableName}";
return await ExecuteRawQueryAsync(query); return await ExecuteRawQueryAsync(query);
} }
public async Task<object?> GetEntityByIdAsync(string entityName, Guid id) 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 hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var query = hasIsDeleted var query = hasIsDeleted
? $"SELECT * FROM [{entityName}] WHERE Id = '{id}' AND (IsDeleted = 0 OR IsDeleted IS NULL)" ? $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}' AND ({isDeletedColumn} = {FalseLiteral(isPostgreSql)} OR {isDeletedColumn} IS NULL)"
: $"SELECT * FROM [{entityName}] WHERE Id = '{id}'"; : $"SELECT * FROM {tableName} WHERE {idColumn} = '{id}'";
var result = await ExecuteRawQueryAsync(query); var result = await ExecuteRawQueryAsync(query);
return result?.FirstOrDefault(); return result?.FirstOrDefault();
} }
public async Task<object?> CreateEntityAsync(string entityName, JsonElement data) public async Task<object?> CreateEntityAsync(string entityName, JsonElement data)
{ {
var dbContext = await _dbContextProvider.GetDbContextAsync();
var isPostgreSql = IsPostgreSql(dbContext);
var newId = Guid.NewGuid(); var newId = Guid.NewGuid();
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted"); var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var hasCreationTime = await ColumnExistsAsync(entityName, "CreationTime"); 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}'" }; var values = new List<string> { $"'{newId}'" };
if (hasCreationTime) { columns.Add("[CreationTime]"); values.Add("SYSUTCDATETIME()"); } if (hasCreationTime) { columns.Add(QuoteIdentifier("CreationTime", isPostgreSql)); values.Add(UtcNowExpression(isPostgreSql)); }
if (hasIsDeleted) { columns.Add("[IsDeleted]"); values.Add("0"); } if (hasIsDeleted) { columns.Add(QuoteIdentifier("IsDeleted", isPostgreSql)); values.Add(FalseLiteral(isPostgreSql)); }
foreach (var prop in data.EnumerateObject()) foreach (var prop in data.EnumerateObject())
{ {
if (prop.NameEquals("id") || prop.NameEquals("Id")) if (prop.NameEquals("id") || prop.NameEquals("Id"))
continue; continue;
columns.Add($"[{prop.Name}]"); columns.Add(QuoteIdentifier(prop.Name, isPostgreSql));
values.Add(FormatValueForSql(prop.Value)); 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); await dbContext.Database.ExecuteSqlRawAsync(insertQuery);
return await GetEntityByIdAsync(entityName, newId); return await GetEntityByIdAsync(entityName, newId);
@ -69,6 +79,8 @@ public class DynamicEntityManager : IDynamicEntityManager
public async Task<object?> UpdateEntityAsync(string entityName, Guid id, JsonElement data) 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); var existing = await GetEntityByIdAsync(entityName, id);
if (existing == null) if (existing == null)
return null; return null;
@ -77,19 +89,18 @@ public class DynamicEntityManager : IDynamicEntityManager
var hasLastModification = await ColumnExistsAsync(entityName, "LastModificationTime"); var hasLastModification = await ColumnExistsAsync(entityName, "LastModificationTime");
if (hasLastModification) if (hasLastModification)
setParts.Add("[LastModificationTime] = SYSUTCDATETIME()"); setParts.Add($"{QuoteIdentifier("LastModificationTime", isPostgreSql)} = {UtcNowExpression(isPostgreSql)}");
foreach (var prop in data.EnumerateObject()) foreach (var prop in data.EnumerateObject())
{ {
if (prop.NameEquals("id") || prop.NameEquals("Id")) if (prop.NameEquals("id") || prop.NameEquals("Id"))
continue; 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); await dbContext.Database.ExecuteSqlRawAsync(updateQuery);
return await GetEntityByIdAsync(entityName, id); return await GetEntityByIdAsync(entityName, id);
@ -102,6 +113,7 @@ public class DynamicEntityManager : IDynamicEntityManager
return false; return false;
var dbContext = await _dbContextProvider.GetDbContextAsync(); var dbContext = await _dbContextProvider.GetDbContextAsync();
var isPostgreSql = IsPostgreSql(dbContext);
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted"); var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
try try
@ -110,21 +122,21 @@ public class DynamicEntityManager : IDynamicEntityManager
{ {
var hasDeletionTime = await ColumnExistsAsync(entityName, "DeletionTime"); var hasDeletionTime = await ColumnExistsAsync(entityName, "DeletionTime");
var softDeleteQuery = hasDeletionTime var softDeleteQuery = hasDeletionTime
? $"UPDATE [{entityName}] SET [IsDeleted] = 1, [DeletionTime] = SYSUTCDATETIME() WHERE Id = '{id}'" ? $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {QuoteIdentifier("IsDeleted", isPostgreSql)} = {TrueLiteral(isPostgreSql)}, {QuoteIdentifier("DeletionTime", isPostgreSql)} = {UtcNowExpression(isPostgreSql)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'"
: $"UPDATE [{entityName}] SET [IsDeleted] = 1 WHERE Id = '{id}'"; : $"UPDATE {QuoteIdentifier(entityName, isPostgreSql)} SET {QuoteIdentifier("IsDeleted", isPostgreSql)} = {TrueLiteral(isPostgreSql)} WHERE {QuoteIdentifier("Id", isPostgreSql)} = '{id}'";
var affected = await dbContext.Database.ExecuteSqlRawAsync(softDeleteQuery); var affected = await dbContext.Database.ExecuteSqlRawAsync(softDeleteQuery);
return affected > 0; return affected > 0;
} }
else 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); var affected = await dbContext.Database.ExecuteSqlRawAsync(hardDeleteQuery);
return affected > 0; return affected > 0;
} }
} }
catch 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); var affected = await dbContext.Database.ExecuteSqlRawAsync(hardDeleteQuery);
return affected > 0; return affected > 0;
} }
@ -132,8 +144,11 @@ public class DynamicEntityManager : IDynamicEntityManager
private async Task<bool> ColumnExistsAsync(string tableName, string columnName) 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 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(); var connection = dbContext.Database.GetDbConnection();
await dbContext.Database.OpenConnectionAsync(); await dbContext.Database.OpenConnectionAsync();
try 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 return value.ValueKind switch
{ {
JsonValueKind.Number when value.TryGetInt64(out var l) => l.ToString(), 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.Number when value.TryGetDecimal(out var d) => d.ToString(System.Globalization.CultureInfo.InvariantCulture),
JsonValueKind.True => "1", JsonValueKind.True => TrueLiteral(isPostgreSql),
JsonValueKind.False => "0", JsonValueKind.False => FalseLiteral(isPostgreSql),
JsonValueKind.Null => "NULL", JsonValueKind.Null => "NULL",
JsonValueKind.String when value.TryGetGuid(out var g) => $"'{g}'", JsonValueKind.String when value.TryGetGuid(out var g) => $"'{g}'",
JsonValueKind.String when value.TryGetDateTime(out var dt) => $"'{dt:yyyy-MM-dd HH:mm:ss}'", JsonValueKind.String when value.TryGetDateTime(out var dt) => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
@ -196,5 +211,32 @@ public class DynamicEntityManager : IDynamicEntityManager
_ => "NULL", _ => "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()";
}
} }

View file

@ -2,9 +2,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Data.Common; using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform;
using Npgsql; using Npgsql;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading; using Volo.Abp.Threading;
@ -20,6 +24,10 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
private readonly Dictionary<string, DbTransaction> _transactions; private readonly Dictionary<string, DbTransaction> _transactions;
private readonly Dictionary<string, NpgsqlConnection> _connections; 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 bool IsDisposed { get; private set; }
public PgDynamicDataRepository( public PgDynamicDataRepository(
@ -30,6 +38,8 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
_cancellationTokenProvider = cancellationTokenProvider; _cancellationTokenProvider = cancellationTokenProvider;
_transactions = []; _transactions = [];
_connections = []; _connections = [];
_registeredTransactions = [];
_registeredConnections = [];
} }
private string BuildKey(string cs) private string BuildKey(string cs)
@ -41,44 +51,72 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
private async Task<NpgsqlConnection> GetOrCreateConnectionAsync(string cs) private async Task<NpgsqlConnection> GetOrCreateConnectionAsync(string cs)
{ {
var key = BuildKey(cs); var key = BuildKey(cs);
if (!_connections.TryGetValue(key, out var connection)) NpgsqlConnection connection;
lock (_lock)
{ {
connection = new NpgsqlConnection(cs); if (_connections.TryGetValue(key, out connection))
_connections[key] = connection; {
// varsa aynı connection'ı kullan
}
else
{
connection = new NpgsqlConnection(cs);
_connections[key] = connection;
}
} }
// Lock dışında state yönetimi
if (connection.State == ConnectionState.Broken) if (connection.State == ConnectionState.Broken)
{ {
connection.Close(); connection.Close();
} }
if (connection.State != ConnectionState.Open) if (connection.State == ConnectionState.Closed)
{ {
await connection.OpenAsync(_cancellationTokenProvider.FallbackToProvider(default)); await connection.OpenAsync(_cancellationTokenProvider.FallbackToProvider(default));
} }
// UoW tamamlandığında connection'ı kapatmak için tek seferlik kayıt
if (_unitOfWorkManager.Current != null) if (_unitOfWorkManager.Current != null)
{ {
_unitOfWorkManager.Current.OnCompleted(async () => lock (_lock)
{ {
if (_connections.TryGetValue(key, out var conn)) if (!_registeredConnections.Contains(key))
{ {
_connections.Remove(key); _registeredConnections.Add(key);
try _unitOfWorkManager.Current.OnCompleted(async () =>
{ {
if (conn.State != ConnectionState.Closed) NpgsqlConnection conn = null;
lock (_lock)
{ {
await conn.CloseAsync(); if (_connections.TryGetValue(key, out conn))
{
_connections.Remove(key);
}
_registeredConnections.Remove(key);
} }
conn.Dispose();
} if (conn != null)
catch {
{ try
// ignore {
} if (conn.State != ConnectionState.Closed)
{
await conn.CloseAsync();
}
conn.Dispose();
}
catch
{
// ignore
}
}
});
} }
}); }
} }
return connection; return connection;
@ -88,44 +126,62 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
{ {
var key = BuildKey(cs); var key = BuildKey(cs);
if (_transactions.TryGetValue(key, out var tx)) lock (_lock)
{ {
if (tx?.Connection != null && if (_transactions.TryGetValue(key, out var existing))
tx.Connection == con &&
tx.Connection.State == ConnectionState.Open)
{ {
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); _transactions.Remove(key);
}
} }
var newTx = await con.BeginTransactionAsync(_cancellationTokenProvider.FallbackToProvider(default)); var newTransaction = await con.BeginTransactionAsync(_cancellationTokenProvider.FallbackToProvider(default));
_transactions[key] = newTx; 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( _unitOfWorkManager.Current.AddTransactionApi(
key, key,
new DapperTransactionApi(newTx, _cancellationTokenProvider) new DapperTransactionApi(newTransaction, _cancellationTokenProvider)
); );
_unitOfWorkManager.Current.OnCompleted(() => _unitOfWorkManager.Current.OnCompleted(() =>
{ {
_transactions.Remove(key); lock (_lock)
{
_transactions.Remove(key);
_registeredTransactions.Remove(key);
}
return Task.CompletedTask; return Task.CompletedTask;
}); });
} }
return newTx; return newTransaction;
} }
// ------------------ Dapper metotları ------------------ // ------------------ Dapper metotları ------------------
public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null) 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 dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, 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) 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 dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, 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) 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 dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, 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) 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 dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, 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) 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 dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
return await dbConnection.ExecuteAsync(sql, param, transaction); 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 ------------------ // ------------------ Dispose ------------------
public void Dispose() public void Dispose()
@ -208,31 +508,36 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency
if (disposing) if (disposing)
{ {
foreach (var tx in _transactions.Values) lock (_lock)
{ {
try { tx?.Dispose(); } catch { } foreach (var tx in _transactions.Values)
}
_transactions.Clear();
foreach (var connection in _connections.Values)
{
try
{ {
if (connection != null) try { tx?.Dispose(); } catch { }
}
_transactions.Clear();
_registeredTransactions.Clear();
foreach (var connection in _connections.Values)
{
try
{ {
if (connection.State != ConnectionState.Closed) if (connection != null)
{ {
connection.Close(); if (connection.State != ConnectionState.Closed)
{
connection.Close();
}
connection.Dispose();
} }
connection.Dispose(); }
catch
{
// ignore
} }
} }
catch _connections.Clear();
{ _registeredConnections.Clear();
// ignore
}
} }
_connections.Clear();
} }
IsDisposed = true; IsDisposed = true;

View file

@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Sozsoft.Platform.Data; using Sozsoft.Platform.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.EntityFrameworkCore; namespace Sozsoft.Platform.EntityFrameworkCore;
@ -26,10 +27,17 @@ public class EntityFrameworkCorePlatformDbSchemaMigrator
* current scope. * current scope.
*/ */
await _serviceProvider var database = _serviceProvider
.GetRequiredService<PlatformDbContext>() .GetRequiredService<PlatformDbContext>()
.Database .Database;
.MigrateAsync();
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
{
await database.EnsureCreatedAsync();
return;
}
await database.MigrateAsync();
} }
} }

View file

@ -38,6 +38,8 @@ public class PlatformDbContext :
IIdentityDbContext, IIdentityDbContext,
ITenantManagementDbContext ITenantManagementDbContext
{ {
private readonly bool _isPostgreSql;
#region Saas #region Saas
public DbSet<LogEntry> LogEntries { get; set; } public DbSet<LogEntry> LogEntries { get; set; }
public DbSet<Tenant> Tenants { get; set; } public DbSet<Tenant> Tenants { get; set; }
@ -144,14 +146,15 @@ public class PlatformDbContext :
public PlatformDbContext(DbContextOptions<PlatformDbContext> options) public PlatformDbContext(DbContextOptions<PlatformDbContext> options)
: base(options) : base(options)
{ {
_isPostgreSql = options.Extensions.Any(extension =>
extension.GetType().Namespace?.StartsWith("Npgsql.EntityFrameworkCore.PostgreSQL", StringComparison.Ordinal) == true);
} }
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{ {
base.ConfigureConventions(configurationBuilder); base.ConfigureConventions(configurationBuilder);
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql) if (_isPostgreSql)
{ {
configurationBuilder.Properties<string>().UseCollation("tr-x-icu"); 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"); 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");
}
}
} }
} }

View file

@ -16,27 +16,45 @@ public class PlatformDbContextFactory : IDesignTimeDbContextFactory<PlatformDbCo
PlatformEfCoreEntityExtensionMappings.Configure(); PlatformEfCoreEntityExtensionMappings.Configure();
var configuration = BuildConfiguration(); var configuration = BuildConfiguration();
var databaseProvider = GetDatabaseProvider(args);
var builder = new DbContextOptionsBuilder<PlatformDbContext>(); var builder = new DbContextOptionsBuilder<PlatformDbContext>();
switch (DefaultDatabaseProvider) switch (databaseProvider)
{ {
case DatabaseProvider.PostgreSql: case DatabaseProvider.PostgreSql:
//AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); // PGSQL AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
builder.UseNpgsql(configuration.GetConnectionString(DefaultDatabaseProvider)); builder.UseNpgsql(configuration.GetConnectionString(DatabaseProvider.PostgreSql));
break; break;
case DatabaseProvider.SqlServer: case DatabaseProvider.SqlServer:
builder.UseSqlServer(configuration.GetConnectionString(DefaultDatabaseProvider)); builder.UseSqlServer(configuration.GetConnectionString(DatabaseProvider.SqlServer));
break; break;
default: default:
throw new InvalidOperationException("Unsupported database provider configured."); throw new InvalidOperationException($"Unsupported database provider configured: {databaseProvider}");
} }
return new PlatformDbContext(builder.Options); 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() private static IConfigurationRoot BuildConfiguration()
{ {

File diff suppressed because it is too large Load diff

View file

@ -202,7 +202,7 @@
"Summary": "blog.posts.ai.excerpt", "Summary": "blog.posts.ai.excerpt",
"CoverImage": "https://images.pexels.com/photos/8386434/pexels-photo-8386434.jpeg?auto=compress&cs=tinysrgb&w=1920", "CoverImage": "https://images.pexels.com/photos/8386434/pexels-photo-8386434.jpeg?auto=compress&cs=tinysrgb&w=1920",
"CategoryName": "blog.categories.technology", "CategoryName": "blog.categories.technology",
"Author": "system@sozsoft.com" "Author": "SYSTEM@SOZSOFT.COM"
}, },
{ {
"Title": "blog.posts.web.title", "Title": "blog.posts.web.title",
@ -213,7 +213,7 @@
"Summary": "blog.posts.web.excerpt", "Summary": "blog.posts.web.excerpt",
"CoverImage": "https://images.pexels.com/photos/11035471/pexels-photo-11035471.jpeg?auto=compress&cs=tinysrgb&w=1920", "CoverImage": "https://images.pexels.com/photos/11035471/pexels-photo-11035471.jpeg?auto=compress&cs=tinysrgb&w=1920",
"CategoryName": "blog.categories.webdev", "CategoryName": "blog.categories.webdev",
"Author": "system@sozsoft.com" "Author": "SYSTEM@SOZSOFT.COM"
}, },
{ {
"Title": "blog.posts.security.title", "Title": "blog.posts.security.title",
@ -224,7 +224,7 @@
"Summary": "blog.posts.security.excerpt", "Summary": "blog.posts.security.excerpt",
"CoverImage": "https://images.pexels.com/photos/5380642/pexels-photo-5380642.jpeg?auto=compress&cs=tinysrgb&w=1920", "CoverImage": "https://images.pexels.com/photos/5380642/pexels-photo-5380642.jpeg?auto=compress&cs=tinysrgb&w=1920",
"CategoryName": "blog.categories.security", "CategoryName": "blog.categories.security",
"Author": "system@sozsoft.com" "Author": "SYSTEM@SOZSOFT.COM"
}, },
{ {
"Title": "blog.posts.mobile.title", "Title": "blog.posts.mobile.title",
@ -235,7 +235,7 @@
"ReadTime": "4 dk", "ReadTime": "4 dk",
"CoverImage": "https://images.pexels.com/photos/13017583/pexels-photo-13017583.jpeg?auto=compress&cs=tinysrgb&w=1920", "CoverImage": "https://images.pexels.com/photos/13017583/pexels-photo-13017583.jpeg?auto=compress&cs=tinysrgb&w=1920",
"CategoryName": "blog.categories.mobile", "CategoryName": "blog.categories.mobile",
"Author": "system@sozsoft.com" "Author": "SYSTEM@SOZSOFT.COM"
}, },
{ {
"Title": "blog.posts.database.title", "Title": "blog.posts.database.title",
@ -246,7 +246,7 @@
"ReadTime": "8 dk", "ReadTime": "8 dk",
"CoverImage": "https://images.pexels.com/photos/325229/pexels-photo-325229.jpeg?auto=compress&cs=tinysrgb&w=1920", "CoverImage": "https://images.pexels.com/photos/325229/pexels-photo-325229.jpeg?auto=compress&cs=tinysrgb&w=1920",
"CategoryName": "blog.categories.database", "CategoryName": "blog.categories.database",
"Author": "system@sozsoft.com" "Author": "SYSTEM@SOZSOFT.COM"
}, },
{ {
"Title": "blog.posts.digital.title", "Title": "blog.posts.digital.title",
@ -257,7 +257,7 @@
"ReadTime": "6 dk", "ReadTime": "6 dk",
"CoverImage": "https://images.pexels.com/photos/7681091/pexels-photo-7681091.jpeg?auto=compress&cs=tinysrgb&w=1920", "CoverImage": "https://images.pexels.com/photos/7681091/pexels-photo-7681091.jpeg?auto=compress&cs=tinysrgb&w=1920",
"CategoryName": "blog.categories.digital", "CategoryName": "blog.categories.digital",
"Author": "system@sozsoft.com" "Author": "SYSTEM@SOZSOFT.COM"
} }
], ],
"GlobalSearch": [ "GlobalSearch": [
@ -341,7 +341,7 @@
"PermissionsJson": [ "PermissionsJson": [
{ {
"ResourceType": "User", "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ıılış törenimize davet ediyoruz.", "content": "Ankara ofisimiz 1 Kasım tarihinde hizmete başlıyor! Tüm çalışanlarımızıılış törenimize davet ediyoruz.",
"excerpt": "Ankara ofisimiz 1 Kasım tarihinde hizmete başlıyor!", "excerpt": "Ankara ofisimiz 1 Kasım tarihinde hizmete başlıyor!",
"category": "general", "category": "general",
"userName": "system@sozsoft.com", "userName": "SYSTEM@SOZSOFT.COM",
"publishDate": "12-10-2024", "publishDate": "12-10-2024",
"isPinned": true, "isPinned": true,
"viewCount": 0, "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.", "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.", "excerpt": "Yıl sonu performans değerlendirmeleri başlıyor.",
"category": "event", "category": "event",
"userName": "system@sozsoft.com", "userName": "SYSTEM@SOZSOFT.COM",
"publishDate": "08-10-2024", "publishDate": "08-10-2024",
"expiryDate": "05-11-2024", "expiryDate": "05-11-2024",
"isPinned": true, "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.", "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ı", "excerpt": "Cumartesi gecesi planlı bakım çalışması",
"category": "urgent", "category": "urgent",
"userName": "system@sozsoft.com", "userName": "SYSTEM@SOZSOFT.COM",
"publishDate": "08-10-2024", "publishDate": "08-10-2024",
"isPinned": false, "isPinned": false,
"viewCount": 0 "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.", "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ı", "excerpt": "React İleri Seviye eğitimi kayıtları başladı",
"category": "event", "category": "event",
"userName": "system@sozsoft.com", "userName": "SYSTEM@SOZSOFT.COM",
"publishDate": "09-10-2024", "publishDate": "09-10-2024",
"isPinned": false, "isPinned": false,
"viewCount": 0 "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.", "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", "excerpt": "Güvenlik politikası güncellendi - Onay gerekli",
"category": "urgent", "category": "urgent",
"userName": "system@sozsoft.com", "userName": "SYSTEM@SOZSOFT.COM",
"publishDate": "04-10-2024", "publishDate": "04-10-2024",
"isPinned": true, "isPinned": true,
"viewCount": 0 "viewCount": 0
@ -1514,42 +1514,42 @@
"SocialPosts": [ "SocialPosts": [
{ {
"content": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀", "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, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "isOwnPost": true
}, },
{ {
"content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "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, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "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! 🎨", "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, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "isOwnPost": true
}, },
{ {
"content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪", "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, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "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.", "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, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "isOwnPost": true
}, },
{ {
"content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯", "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, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "isOwnPost": true
@ -1635,53 +1635,53 @@
"SocialComments": [ "SocialComments": [
{ {
"postContent": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀", "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 👏" "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! 🚀", "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!" "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!", "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!" "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! 🎨", "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 😍" "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! 🎨", "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ı?" "content": "Dark mode opsiyonu da olacak mı?"
}, },
{ {
"postContent": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪", "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?" "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.", "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?" "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.", "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." "content": "Gelecek hafta başlıyoruz! Kayıt linki mail ile paylaşılacak."
} }
], ],
"SocialLikes": [ "SocialLikes": [
{ {
"postContent": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀", "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! 🎨", "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": [ "EventTypes": [
@ -1862,7 +1862,7 @@
"Description": "Tüm departmanların katılımıyla düzenlenen geleneksel yaz futbol turnuvası.", "Description": "Tüm departmanların katılımıyla düzenlenen geleneksel yaz futbol turnuvası.",
"Place": "Şirket Kampüsü Spor Alanı", "Place": "Şirket Kampüsü Spor Alanı",
"Status": "published", "Status": "published",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"ParticipantsCount": 64, "ParticipantsCount": 64,
"IsPublished": true, "IsPublished": true,
"Likes": 0, "Likes": 0,
@ -1877,7 +1877,7 @@
"Description": "Çalışanlarımıza özel, rehber eşliğinde 2 günlük kültürel gezi.", "Description": "Çalışanlarımıza özel, rehber eşliğinde 2 günlük kültürel gezi.",
"Place": "Kapadokya, Nevşehir", "Place": "Kapadokya, Nevşehir",
"Status": "published", "Status": "published",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"ParticipantsCount": 25, "ParticipantsCount": 25,
"IsPublished": true, "IsPublished": true,
"Likes": 0, "Likes": 0,
@ -1892,7 +1892,7 @@
"Description": "Caz müziğinin en güzel örneklerinin canlı performanslarla sunulacağı özel akşam.", "Description": "Caz müziğinin en güzel örneklerinin canlı performanslarla sunulacağı özel akşam.",
"Place": "Şirket Konferans Salonu", "Place": "Şirket Konferans Salonu",
"Status": "published", "Status": "published",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"ParticipantsCount": 40, "ParticipantsCount": 40,
"IsPublished": true, "IsPublished": true,
"Likes": 0, "Likes": 0,
@ -1903,55 +1903,55 @@
"EventComments": [ "EventComments": [
{ {
"EventName": "Yaz Futbol Turnuvası 2025", "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 🎉", "Content": "Muhteşem bir gündü! Yılın en güzel etkinliği 🎉",
"Likes": 12 "Likes": 12
}, },
{ {
"EventName": "Yaz Futbol Turnuvası 2025", "EventName": "Yaz Futbol Turnuvası 2025",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"Content": "Voleybol turnuvası harikaydı, gelecek yıl yine yapalım!", "Content": "Voleybol turnuvası harikaydı, gelecek yıl yine yapalım!",
"Likes": 8 "Likes": 8
}, },
{ {
"EventName": "Kültür Gezisi: Kapadokya", "EventName": "Kültür Gezisi: Kapadokya",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"Content": "Ekibimiz 2. oldu! Çok gurur duydum herkesle 💪", "Content": "Ekibimiz 2. oldu! Çok gurur duydum herkesle 💪",
"Likes": 15 "Likes": 15
}, },
{ {
"EventName": "Kültür Gezisi: Kapadokya", "EventName": "Kültür Gezisi: Kapadokya",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"Content": "Gece boyunca kod yazmak ve pizza yemek priceless! 🍕", "Content": "Gece boyunca kod yazmak ve pizza yemek priceless! 🍕",
"Likes": 10 "Likes": 10
}, },
{ {
"EventName": "Müzik Dinletisi: Jazz Akşamı", "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 🏆", "Content": "İT departmanı şampiyon oldu! Gelecek sene kupayı koruyacağız 🏆",
"Likes": 18 "Likes": 18
}, },
{ {
"EventName": "Müzik Dinletisi: Jazz Akşamı", "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 👏", "Content": "Yılın en şık gecesi! Organizasyon mükemmeldi 👏",
"Likes": 25 "Likes": 25
}, },
{ {
"EventName": "Müzik Dinletisi: Jazz Akşamı", "EventName": "Müzik Dinletisi: Jazz Akşamı",
"UserName": "system@sozsoft.com", "UserName": "SYSTEM@SOZSOFT.COM",
"Content": "Tombala hediyelerim harika, çok teşekkürler! 🎁", "Content": "Tombala hediyelerim harika, çok teşekkürler! 🎁",
"Likes": 14 "Likes": 14
}, },
{ {
"EventName": "Müzik Dinletisi: Jazz Akşamı", "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! 🎵", "Content": "Müzik grubunuz süperdi, dans pistinden ayrılamadık! 🎵",
"Likes": 19 "Likes": 19
}, },
{ {
"EventName": "Müzik Dinletisi: Jazz Akşamı", "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 🎨", "Content": "İlk defa ebru yaptım, çok huzurlu bir deneyimdi 🎨",
"Likes": 11 "Likes": 11
} }

View file

@ -10,6 +10,7 @@ using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Npgsql;
using Serilog; using Serilog;
using static Sozsoft.Settings.SettingsConsts; using static Sozsoft.Settings.SettingsConsts;
@ -38,9 +39,10 @@ internal static class SetupAppRunner
if (DefaultDatabaseProvider == DatabaseProvider.SqlServer) if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
return SqlServerIsReady(connectionString); return SqlServerIsReady(connectionString);
#pragma warning disable CS0162 if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
return true; // Diğer sağlayıcılar için geçici — ileride PostgreSQL desteği eklenecek return PostgreSqlIsReady(connectionString);
#pragma warning restore CS0162
return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -83,6 +85,46 @@ internal static class SetupAppRunner
return (int)tableCheck.ExecuteScalar() > 0; 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ı // Minimal Kurulum Uygulaması
public static async Task<int> RunAsync(string[] args, IConfiguration configuration) public static async Task<int> RunAsync(string[] args, IConfiguration configuration)

View file

@ -337,6 +337,12 @@ public class PlatformHttpApiHostModule : AbpModule
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{ {
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
if (connectionString.IsNullOrWhiteSpace() || !SetupAppRunner.DatabaseIsReady(configuration))
{
return;
}
Configure<AbpHangfireOptions>(options => Configure<AbpHangfireOptions>(options =>
{ {
options.ServerOptions = new BackgroundJobServerOptions 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 CS0162 // Unreachable code detected
#pragma warning disable CS0618 // Type or member is obsolete
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql) if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
{ {
context.Services.AddHangfire(options => context.Services.AddHangfire(options =>
{ {
options.UsePostgreSqlStorage( options.UsePostgreSqlStorage(
configuration.GetConnectionString(DefaultDatabaseProvider), storageOptions => storageOptions.UseNpgsqlConnection(connectionString),
new PostgreSqlStorageOptions new PostgreSqlStorageOptions
{ {
PrepareSchemaIfNecessary = true PrepareSchemaIfNecessary = true
}); });
}); });
} }
#pragma warning restore CS0618
#pragma warning restore CS0162
else if (DefaultDatabaseProvider == DatabaseProvider.SqlServer) else if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
{ {
context.Services.AddHangfire(options => context.Services.AddHangfire(options =>
{ {
options.UseSqlServerStorage( options.UseSqlServerStorage(
configuration.GetConnectionString(DefaultDatabaseProvider), connectionString,
new SqlServerStorageOptions new SqlServerStorageOptions
{ {
PrepareSchemaIfNecessary = true, PrepareSchemaIfNecessary = true,
@ -379,9 +379,10 @@ public class PlatformHttpApiHostModule : AbpModule
QueuePollInterval = TimeSpan.Zero, QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true, UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true DisableGlobalLocks = true
}); });
}); });
} }
#pragma warning restore CS0162
} }

View file

@ -19,6 +19,8 @@ public class Program
{ {
public async static Task<int> Main(string[] args) public async static Task<int> Main(string[] args)
{ {
ConfigurePostgreSqlCompatibility();
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) .SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json") .AddJsonFile("appsettings.json")
@ -191,6 +193,14 @@ public class Program
Log.CloseAndFlush(); Log.CloseAndFlush();
} }
} }
private static void ConfigurePostgreSqlCompatibility()
{
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
}
}
} }

View file

@ -0,0 +1 @@
docker compose -f .\docker-compose-data.yml --profile postgres up -d

View file

@ -0,0 +1 @@
docker compose -f .\docker-compose-data.yml --profile postgres down -v

View file

@ -0,0 +1 @@
docker compose -f .\docker-compose-data.yml --profile sql down -v

View file

@ -1,6 +1,17 @@
{ {
"commit": "8f3932b", "commit": "0b5eb3d",
"releases": [ "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", "version": "1.0.10",
"buildDate": "2026-05-11", "buildDate": "2026-05-11",

View file

@ -27,12 +27,14 @@ export interface SqlTemplateDto {
export interface DatabaseTableDto { export interface DatabaseTableDto {
schemaName: string schemaName: string
tableName: string tableName: string
dataSourceType?: string
fullName: string fullName: string
} }
export interface SqlNativeObjectDto { export interface SqlNativeObjectDto {
schemaName: string schemaName: string
objectName: string objectName: string
dataSourceType?: string
fullName: string fullName: string
} }

View file

@ -39,6 +39,16 @@ export class SqlObjectManagerService {
{ apiName: this.apiName, ...config }, { 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>) => getNativeObjectDefinition = (dataSourceCode: string, schemaName: string, objectName: string, config?: Partial<Config>) =>
apiService.fetchData<string, void>( apiService.fetchData<string, void>(
{ {

View file

@ -13,6 +13,7 @@ import {
FaTrash, FaTrash,
} from 'react-icons/fa' } from 'react-icons/fa'
import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models' 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 { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
@ -29,6 +30,7 @@ interface TreeNode {
interface SqlObjectExplorerProps { interface SqlObjectExplorerProps {
dataSource: string | null dataSource: string | null
dataSourceType?: DataSourceTypeEnum
onTemplateSelect?: (template: string, templateType: string) => void onTemplateSelect?: (template: string, templateType: string) => void
onViewDefinition?: (schemaName: string, objectName: string) => void onViewDefinition?: (schemaName: string, objectName: string) => void
onGenerateTableScript?: (schemaName: string, tableName: string) => void onGenerateTableScript?: (schemaName: string, tableName: string) => void
@ -55,6 +57,7 @@ const FOLDER_META: Record<FolderKey, { label: string; color: string }> = {
const SqlObjectExplorer = ({ const SqlObjectExplorer = ({
dataSource, dataSource,
dataSourceType,
onTemplateSelect, onTemplateSelect,
onViewDefinition, onViewDefinition,
onGenerateTableScript, onGenerateTableScript,
@ -64,6 +67,7 @@ const SqlObjectExplorer = ({
refreshTrigger, refreshTrigger,
}: SqlObjectExplorerProps) => { }: SqlObjectExplorerProps) => {
const { translate } = useLocalization() const { translate } = useLocalization()
const isPostgreSql = dataSourceType === DataSourceTypeEnum.Postgresql
const [treeData, setTreeData] = useState<TreeNode[]>([]) const [treeData, setTreeData] = useState<TreeNode[]>([])
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set(['root'])) const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set(['root']))
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -182,7 +186,13 @@ const SqlObjectExplorer = ({
if (node.folder === 'tables') { if (node.folder === 'tables') {
// Generate SELECT template for tables // Generate SELECT template for tables
const t = node.data as DatabaseTableDto 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 { } else {
// Load native object definition into editor // Load native object definition into editor
const obj = node.data as SqlNativeObjectDto const obj = node.data as SqlNativeObjectDto
@ -250,14 +260,16 @@ const SqlObjectExplorer = ({
const buildDropSql = (node: TreeNode): string => { const buildDropSql = (node: TreeNode): string => {
if (node.folder === 'tables') { if (node.folder === 'tables') {
const t = node.data as DatabaseTableDto 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 obj = node.data as SqlNativeObjectDto
const fullName = obj.fullName ?? (isPostgreSql ? `"${obj.schemaName}"."${obj.objectName}"` : `[${obj.schemaName}].[${obj.objectName}]`)
const keyword = const keyword =
node.folder === 'views' ? 'VIEW' : node.folder === 'views' ? 'VIEW' :
node.folder === 'procedures' ? 'PROCEDURE' : node.folder === 'procedures' ? 'PROCEDURE' :
'FUNCTION' '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[] => { const getSqlDataFileCandidates = (node: TreeNode): string[] => {

View file

@ -4,6 +4,7 @@ import Container from '@/components/shared/Container'
import ConfirmDialog from '@/components/shared/ConfirmDialog' import ConfirmDialog from '@/components/shared/ConfirmDialog'
import { getDataSources } from '@/services/data-source.service' import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source' import type { DataSourceDto } from '@/proxy/data-source'
import { DataSourceTypeEnum } from '@/proxy/form/models'
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models' import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa' import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa'
@ -139,9 +140,16 @@ const SqlQueryManager = () => {
const escapeSqlLiteral = (value: string) => value.replace(/'/g, "''") const escapeSqlLiteral = (value: string) => value.replace(/'/g, "''")
const escapeSqlIdentifier = (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) => const getSafeFullName = (schemaName: string, objectName: string) =>
`[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]` `[${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 buildTableScriptQuery = (schemaName: string, tableName: string) => {
const fullName = getSafeFullName(schemaName, tableName) const fullName = getSafeFullName(schemaName, tableName)
@ -231,23 +239,40 @@ SELECT
const getTableCreateScript = async (schemaName: string, tableName: string): Promise<string> => { const getTableCreateScript = async (schemaName: string, tableName: string): Promise<string> => {
if (!state.selectedDataSource) return '' if (!state.selectedDataSource) return ''
const result = await sqlObjectManagerService.executeQuery({ const result = await sqlObjectManagerService.getTableCreateScript(
queryText: buildTableScriptQuery(schemaName, tableName), state.selectedDataSource,
dataSourceCode: state.selectedDataSource, schemaName,
}) tableName,
)
const firstRow = result.data?.data?.[0] return result.data || ''
if (!firstRow) return ''
return firstRow.Script || firstRow.script || ''
} }
const normalizeNativeDefinitionToCreate = (definition: string) => { const normalizeNativeDefinitionToCreate = (definition: string) => {
if (!definition?.trim()) return '' if (!definition?.trim()) return ''
if (isPostgreSql) return definition
return definition.replace(/^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?/i, 'CREATE OR ALTER ') return definition.replace(/^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?/i, 'CREATE OR ALTER ')
} }
const buildDropIfExistsScript = (obj: SqlExplorerSelectedObject) => { 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) const fullName = getSafeFullName(obj.schemaName, obj.objectName)
if (obj.objectType === 'table') { if (obj.objectType === 'table') {
@ -266,6 +291,33 @@ SELECT
} }
const buildObjectExistsCheckQuery = (obj: SqlExplorerSelectedObject) => { 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 fullName = getSafeFullName(obj.schemaName, obj.objectName)
const escapedFullName = escapeSqlLiteral(fullName) const escapedFullName = escapeSqlLiteral(fullName)
@ -299,6 +351,88 @@ SELECT
} }
const getTemplateContent = (templateType: string): string => { 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> = { const templates: Record<string, string> = {
select: `-- Basic SELECT query select: `-- Basic SELECT query
SELECT SELECT
@ -981,6 +1115,7 @@ GO`,
<div className="flex-1 min-h-0 flex flex-col overflow-hidden"> <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<SqlObjectExplorer <SqlObjectExplorer
dataSource={state.selectedDataSource} dataSource={state.selectedDataSource}
dataSourceType={selectedDataSourceType}
onTemplateSelect={handleTemplateSelect} onTemplateSelect={handleTemplateSelect}
onViewDefinition={handleViewDefinition} onViewDefinition={handleViewDefinition}
onGenerateTableScript={handleGenerateTableScript} onGenerateTableScript={handleGenerateTableScript}

View file

@ -35,6 +35,14 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
const [hoveredCommentAuthor, setHoveredCommentAuthor] = useState<string | null>(null) const [hoveredCommentAuthor, setHoveredCommentAuthor] = useState<string | null>(null)
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { user } = useStoreState((state) => state.auth) 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 // Intersection Observer for video autoplay/pause
useEffect(() => { useEffect(() => {
@ -177,7 +185,8 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
case 'poll': case 'poll':
if (post.media.pollQuestion && post.media.pollOptions) { 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 hasVoted = !!post.media.pollUserVoteId
const totalVotes = post.media.pollTotalVotes || 0 const totalVotes = post.media.pollTotalVotes || 0
const pollUserVoteId = post.media.pollUserVoteId const pollUserVoteId = post.media.pollUserVoteId
@ -189,13 +198,13 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
</h4> </h4>
<div className="space-y-2"> <div className="space-y-2">
{post.media.pollOptions.map((option) => { {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 const isSelected = pollUserVoteId === option.id
return ( return (
<button <button
key={option.id} key={option.id}
onClick={() => !hasVoted && !isExpired && onVote(post.id, option.id)} onClick={() => option.id && !hasVoted && !isExpired && onVote(post.id, option.id)}
disabled={hasVoted || isExpired} disabled={hasVoted || isExpired}
className={classNames( className={classNames(
'w-full text-left p-3 rounded-lg relative overflow-hidden transition-all', '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 {' '} {totalVotes} oy {' '}
{isExpired {isExpired
? 'Sona erdi' ? 'Sona erdi'
: post.media.pollEndsAt : pollEndsAt
? dayjs(post.media.pollEndsAt).fromNow() + ' bitiyor' ? dayjs(pollEndsAt).fromNow() + ' bitiyor'
: ''} : ''}
</div> </div>
</div> </div>
@ -259,18 +268,18 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
onMouseEnter={() => setShowUserCard(true)} onMouseEnter={() => setShowUserCard(true)}
onMouseLeave={() => setShowUserCard(false)} 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> <AnimatePresence>
{showUserCard && ( {showUserCard && (
<UserProfileCard <UserProfileCard
user={{ user={{
id: post.user.id || '', id: postUserId,
name: post.user.fullName || '', name: postUserFullName,
title: post.user.jobPositions?.[0]?.name || '', title: postUserTitle,
email: post.user.email, email: postUser.email,
phoneNumber: post.user.phoneNumber, phoneNumber: postUser.phoneNumber,
department: post.user.departments?.[0]?.name, department: postUser.departments?.[0]?.name,
tenantId: post.user.tenantId || '', tenantId: postUserTenantId,
}} }}
position="bottom" position="bottom"
/> />
@ -278,9 +287,9 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
</AnimatePresence> </AnimatePresence>
</div> </div>
<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"> <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> </p>
</div> </div>
</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" />} {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>
<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" 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" /> <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> </button>
</div> </div>
@ -358,7 +367,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
{/* Comments List */} {/* Comments List */}
<div className="space-y-3"> <div className="space-y-3">
{post.comments.map((comment) => ( {postComments.map((comment) => (
<div key={comment.id} className="flex gap-3"> <div key={comment.id} className="flex gap-3">
<div <div
className="relative" className="relative"
@ -368,16 +377,16 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
<Avatar <Avatar
size={32} size={32}
shape="circle" shape="circle"
src={AVATAR_URL(comment.user.id, comment.user.tenantId)} src={AVATAR_URL(comment.user?.id ?? '', comment.user?.tenantId ?? '')}
/> />
<AnimatePresence> <AnimatePresence>
{hoveredCommentAuthor === comment.id && ( {hoveredCommentAuthor === comment.id && (
<UserProfileCard <UserProfileCard
user={{ user={{
id: comment.user.id || '', id: comment.user?.id || '',
name: comment.user.fullName || '', name: comment.user?.fullName || '',
title: comment.user.jobPositions?.[0]?.name || '', title: comment.user?.jobPositions?.[0]?.name || '',
tenantId: comment.user.tenantId || '', tenantId: comment.user?.tenantId || '',
}} }}
position="bottom" position="bottom"
/> />
@ -387,7 +396,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
<div className="flex-1"> <div className="flex-1">
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2"> <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"> <h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{comment.user.fullName} {comment.user?.fullName || '-'}
</h4> </h4>
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p> <p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
</div> </div>

View file

@ -38,7 +38,14 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
intranetService.incrementAnnouncementViewCount(announcement.id) intranetService.incrementAnnouncementViewCount(announcement.id)
}, [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 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) => const imgSrc = (img: string) =>
img.startsWith('data:') || img.startsWith('http://') || img.startsWith('https://') || img.startsWith('/') 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-1">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<span <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')}`} `📢 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.General')}`}
{announcement.category === 'hr' && {category === 'hr' &&
`👥 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.HR')}`} `👥 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.HR')}`}
{announcement.category === 'it' && {category === 'it' &&
`💻 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.IT')}`} `💻 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.IT')}`}
{announcement.category === 'event' && {category === 'event' &&
`🎉 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Event')}`} `🎉 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Event')}`}
{announcement.category === 'urgent' && {category === 'urgent' &&
`🚨 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Urgent')}`} `🚨 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Urgent')}`}
</span> </span>
{announcement.isPinned && ( {announcement.isPinned && (
@ -115,11 +122,11 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
<Avatar <Avatar
size={32} size={32}
shape="circle" shape="circle"
src={AVATAR_URL(announcement.user.id, announcement.user.tenantId)} src={AVATAR_URL(announcementUser.id ?? '', announcementUser.tenantId ?? '')}
/> />
<div> <div>
<p className="font-semibold text-gray-900 dark:text-white"> <p className="font-semibold text-gray-900 dark:text-white">
{announcement.user.fullName} {announcementUserName}
</p> </p>
<div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
<span>{currentLocalDate(announcement.publishDate, currentLocale || 'tr')}</span> <span>{currentLocalDate(announcement.publishDate, currentLocale || 'tr')}</span>
@ -174,15 +181,15 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onC
</div> </div>
{/* Attachments */} {/* 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"> <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"> <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" /> <FaClipboard className="w-5 h-5" />
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Attachments')} ( {translate('::App.Platform.Intranet.AnnouncementDetailModal.Attachments')} (
{announcement.attachments.length}) {attachments.length})
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
{announcement.attachments.map((attachment, idx) => ( {attachments.map((attachment, idx) => (
<a <a
key={idx} key={idx}
href={attachment.url} href={attachment.url}

View file

@ -12,7 +12,8 @@ interface AnnouncementsProps {
} }
const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnouncementClick }) => { 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 { translate } = useLocalization()
const getCategoryColor = (category: string) => { const getCategoryColor = (category: string) => {
@ -26,6 +27,15 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
return colors[category] || colors.general return colors[category] || colors.general
} }
const getUserName = (announcement: AnnouncementDto) => {
const announcementUser = announcement.user
return (
announcementUser?.fullName ||
[announcementUser?.name, announcementUser?.surname].filter(Boolean).join(' ') ||
'-'
)
}
return ( return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"> <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"> <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-1 min-w-0">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<h3 className="text-base font-semibold text-gray-900 dark:text-white"> <h3 className="text-base font-semibold text-gray-900 dark:text-white">
{announcement.title} {announcement.title || '-'}
</h3> </h3>
{announcement.category && ( {announcement.category && (
<span <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)}` 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> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"> <p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{announcement.excerpt} {announcement.excerpt || ''}
</p> </p>
<div className="flex items-center gap-2 mt-3 text-xs text-gray-500 dark:text-gray-400"> <div className="flex items-center gap-2 mt-3 text-xs text-gray-500 dark:text-gray-400">
<Avatar <Avatar
size={24} size={24}
shape="circle" 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></span>
<span>{dayjs(announcement.publishDate).fromNow()}</span> <span>{dayjs(announcement.publishDate).fromNow()}</span>
<span></span> <span></span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<FaEye className="w-3 h-3" /> <FaEye className="w-3 h-3" />
{announcement.viewCount} {announcement.viewCount ?? 0}
</span> </span>
</div> </div>
</div> </div>

View file

@ -35,10 +35,16 @@ const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick })
const { translate } = useLocalization() const { translate } = useLocalization()
const now = dayjs() const now = dayjs()
const upcomingEvents = events const upcomingEvents = (events ?? [])
.filter((event) => event.isPublished && !dayjs(event.date).isBefore(now, 'day')) .filter((event) => event?.isPublished && !dayjs(event.date).isBefore(now, 'day'))
.sort((left, right) => dayjs(left.date).valueOf() - dayjs(right.date).valueOf()) .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 ( return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"> <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"> <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 <Avatar
size={24} size={24}
shape="circle" 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></span>
<span>{dayjs(event.date).fromNow()}</span> <span>{dayjs(event.date).fromNow()}</span>
{event.likes > 0 && ( {event.likes > 0 && (