CrudEndpoint ve CrudEndpointGenerate düzenlemesi

This commit is contained in:
Sedat Öztürk 2026-03-03 00:34:19 +03:00
parent fe19cacedc
commit 2b96619a1d
22 changed files with 350 additions and 1848 deletions

View file

@ -11,7 +11,6 @@ public class CrudEndpointDto : FullAuditedEntityDto<Guid>
public string OperationType { get; set; } = string.Empty;
public string CsharpCode { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public Guid EntityId { get; set; }
// Navigation property for display purposes
public string? EntityDisplayName { get; set; }
@ -25,6 +24,5 @@ public class CreateUpdateCrudEndpointDto
public string OperationType { get; set; } = string.Empty;
public string CsharpCode { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public Guid EntityId { get; set; }
}

View file

@ -13,7 +13,8 @@ public interface ICrudEndpointAppService : ICrudAppService<
CreateUpdateCrudEndpointDto>
{
Task<List<CrudEndpointDto>> GetActiveEndpointsAsync();
Task<List<CrudEndpointDto>> GetEndpointsByEntityAsync(Guid entityId);
Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(Guid entityId);
Task<List<CrudEndpointDto>> GetEndpointsByEntityAsync(string entityName);
Task<CrudEndpointDto> ToggleAsync(Guid id);
Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName);
}

View file

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.DeveloperKit;
public interface ISqlTableAppService : ICrudAppService<
SqlTableDto,
Guid,
PagedAndSortedResultRequestDto,
CreateUpdateSqlTableDto>
{
Task<List<SqlTableDto>> GetActiveEntitiesAsync();
Task<SqlTableDto> ToggleActiveStatusAsync(Guid id);
}

View file

@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.DeveloperKit;
public class SqlTableDto : FullAuditedEntityDto<Guid>
{
public string Menu { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string TableName { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false;
public string EndpointStatus { get; set; } = "pending";
public List<SqlTableFieldDto> Fields { get; set; } = [];
}
public class CreateUpdateSqlTableDto
{
public string Menu { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string TableName { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false;
public List<CreateUpdateSqlTableFieldDto> Fields { get; set; } = [];
}
public class SqlTableFieldDto : FullAuditedEntityDto<Guid>
{
public Guid EntityId { get; set; }
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool IsRequired { get; set; }
public int? MaxLength { get; set; }
public bool IsUnique { get; set; }
public string? DefaultValue { get; set; }
public string? Description { get; set; }
public int DisplayOrder { get; set; } = 0;
}
public class CreateUpdateSqlTableFieldDto
{
public Guid? Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool IsRequired { get; set; }
public int? MaxLength { get; set; }
public bool IsUnique { get; set; }
public string? DefaultValue { get; set; }
public string? Description { get; set; }
public int DisplayOrder { get; set; } = 0;
}

View file

@ -3,15 +3,15 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.Entities;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform;
namespace Platform.Api.Application;
[Authorize]
public class CrudEndpointGenerateAppService : CrudAppService<
CrudEndpoint,
CrudEndpointDto,
@ -19,116 +19,95 @@ public class CrudEndpointGenerateAppService : CrudAppService<
PagedAndSortedResultRequestDto,
CreateUpdateCrudEndpointDto>, ICrudEndpointAppService
{
private readonly IRepository<SqlTable, Guid> _entityRepository;
private readonly IRepository<CrudEndpoint, Guid> _endpointRepository;
public CrudEndpointGenerateAppService(
IRepository<CrudEndpoint, Guid> repository,
IRepository<SqlTable, Guid> entityRepository,
IRepository<CrudEndpoint, Guid> endpointRepository)
IRepository<CrudEndpoint, Guid> repository)
: base(repository)
{
_entityRepository = entityRepository;
_endpointRepository = endpointRepository;
}
[HttpGet("api/app/crud-endpoint-generate/active-endpoints")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
public virtual async Task<List<CrudEndpointDto>> GetActiveEndpointsAsync()
{
var endpoints = await Repository.GetListAsync(x => x.IsActive);
return await MapToGetListOutputDtosAsync(endpoints);
}
public virtual async Task<List<CrudEndpointDto>> GetEndpointsByEntityAsync(Guid entityId)
[HttpGet("api/app/crud-endpoint-generate/endpoints-by-entity/{entityName}")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
public virtual async Task<List<CrudEndpointDto>> GetEndpointsByEntityAsync(string entityName)
{
var endpoints = await _endpointRepository.GetListAsync(x => x.EntityId == entityId);
var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName);
return ObjectMapper.Map<List<CrudEndpoint>, List<CrudEndpointDto>>(endpoints);
}
public virtual async Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(Guid entityId)
[HttpPost("api/app/crud-endpoint-generate/{id}/toggle")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
public virtual async Task<CrudEndpointDto> ToggleAsync(Guid id)
{
// Entity + Fields
var entityQueryable = await _entityRepository.GetQueryableAsync();
var entity = await entityQueryable
.Include(x => x.Fields)
.FirstOrDefaultAsync(x => x.Id == entityId);
if (entity == null)
{
throw new Exception($"Entity with ID {entityId} not found");
}
var endpoint = await Repository.GetAsync(id);
endpoint.IsActive = !endpoint.IsActive;
await Repository.UpdateAsync(endpoint, autoSave: true);
return ObjectMapper.Map<CrudEndpoint, CrudEndpointDto>(endpoint);
}
[HttpPost("api/app/crud-endpoint-generate/generate-crud-endpoints/{entityName}")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
public virtual async Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName)
{
// CRUD endpointleri oluştur
var endpoints = new List<CrudEndpoint>();
var entityName = entity.Name;
var entityDisplayName = entity.DisplayName;
endpoints.Add(new CrudEndpoint
var endpoints = new List<CrudEndpoint>
{
EntityId = entityId,
EntityName = entityName,
Method = "GET",
Path = $"/api/app/crudendpoint/{entityName.ToLower()}",
OperationType = "GetList",
IsActive = true,
CsharpCode = GenerateGetAllCode(entityName, entityDisplayName)
});
endpoints.Add(new CrudEndpoint
{
EntityId = entityId,
EntityName = entityName,
Method = "GET",
Path = $"/api/app/crudendpoint/{entityName.ToLower()}/{{id}}",
OperationType = "GetById",
IsActive = true,
CsharpCode = GenerateGetByIdCode(entityName, entityDisplayName)
});
endpoints.Add(new CrudEndpoint
{
EntityId = entityId,
EntityName = entityName,
Method = "POST",
Path = $"/api/app/crudendpoint/{entityName.ToLower()}",
OperationType = "Create",
IsActive = true,
CsharpCode = GenerateCreateCode(entityName, entityDisplayName)
});
endpoints.Add(new CrudEndpoint
{
EntityId = entityId,
EntityName = entityName,
Method = "PUT",
Path = $"/api/app/crudendpoint/{entityName.ToLower()}/{{id}}",
OperationType = "Update",
IsActive = true,
CsharpCode = GenerateUpdateCode(entityName, entityDisplayName)
});
endpoints.Add(new CrudEndpoint
{
EntityId = entityId,
EntityName = entityName,
Method = "DELETE",
Path = $"/api/app/crudendpoint/{entityName.ToLower()}/{{id}}",
OperationType = "Delete",
IsActive = true,
CsharpCode = GenerateDeleteCode(entityName, entityDisplayName)
});
new() {
EntityName = entityName,
Method = "GET",
Path = $"/api/app/crudendpoint/{entityName}",
OperationType = "GetList",
IsActive = true,
CsharpCode = GenerateGetAllCode(entityName)
},
new() {
EntityName = entityName,
Method = "GET",
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
OperationType = "GetById",
IsActive = true,
CsharpCode = GenerateGetByIdCode(entityName)
},
new() {
EntityName = entityName,
Method = "POST",
Path = $"/api/app/crudendpoint/{entityName}",
OperationType = "Create",
IsActive = true,
CsharpCode = GenerateCreateCode(entityName)
},
new() {
EntityName = entityName,
Method = "PUT",
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
OperationType = "Update",
IsActive = true,
CsharpCode = GenerateUpdateCode(entityName)
},
new() {
EntityName = entityName,
Method = "DELETE",
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
OperationType = "Delete",
IsActive = true,
CsharpCode = GenerateDeleteCode(entityName)
}
};
// Var olanları sil
var existingEndpoints = await _endpointRepository
.GetListAsync(x => x.EntityId == entityId);
var existingEndpoints = await Repository
.GetListAsync(x => x.EntityName == entityName);
await _endpointRepository.DeleteManyAsync(existingEndpoints);
await Repository.DeleteManyAsync(existingEndpoints);
// Yeni endpointleri ekle
await _endpointRepository.InsertManyAsync(endpoints, autoSave: true);
// Entity endpoint durumu güncelle
entity.EndpointStatus = "Uygulandı";
await _entityRepository.UpdateAsync(entity, autoSave: true);
await Repository.InsertManyAsync(endpoints, autoSave: true);
var result = ObjectMapper.Map<List<CrudEndpoint>, List<CrudEndpointDto>>(endpoints);
@ -139,7 +118,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
};
}
private string GenerateGetAllCode(string entityName, string displayName)
private string GenerateGetAllCode(string entityName)
{
return $@"[HttpGet]
public async Task<ActionResult<List<{entityName}>>> GetAll{entityName}sAsync()
@ -149,7 +128,7 @@ public async Task<ActionResult<List<{entityName}>>> GetAll{entityName}sAsync()
}}";
}
private string GenerateGetByIdCode(string entityName, string displayName)
private string GenerateGetByIdCode(string entityName)
{
return $@"[HttpGet(""{{id}}"")]
public async Task<ActionResult<{entityName}>> Get{entityName}Async(Guid id)
@ -157,13 +136,13 @@ public async Task<ActionResult<{entityName}>> Get{entityName}Async(Guid id)
var entity = await _context.{entityName}s.FindAsync(id);
if (entity == null)
{{
return NotFound($""{displayName} with ID {{id}} not found"");
return NotFound($""{entityName} with ID {{id}} not found"");
}}
return Ok(entity);
}}";
}
private string GenerateCreateCode(string entityName, string displayName)
private string GenerateCreateCode(string entityName)
{
return $@"[HttpPost]
public async Task<ActionResult<{entityName}>> Create{entityName}Async({entityName} {entityName.ToLower()})
@ -174,7 +153,7 @@ public async Task<ActionResult<{entityName}>> Create{entityName}Async({entityNam
}}";
}
private string GenerateUpdateCode(string entityName, string displayName)
private string GenerateUpdateCode(string entityName)
{
return $@"[HttpPut(""{{id}}"")]
public async Task<IActionResult> Update{entityName}Async(Guid id, {entityName} {entityName.ToLower()})
@ -194,7 +173,7 @@ public async Task<IActionResult> Update{entityName}Async(Guid id, {entityName} {
{{
if (!await {entityName}ExistsAsync(id))
{{
return NotFound($""{displayName} with ID {{id}} not found"");
return NotFound($""{entityName} with ID {{id}} not found"");
}}
throw;
}}
@ -203,7 +182,7 @@ public async Task<IActionResult> Update{entityName}Async(Guid id, {entityName} {
}}";
}
private string GenerateDeleteCode(string entityName, string displayName)
private string GenerateDeleteCode(string entityName)
{
return $@"[HttpDelete(""{{id}}"")]
public async Task<IActionResult> Delete{entityName}Async(Guid id)
@ -211,7 +190,7 @@ public async Task<IActionResult> Delete{entityName}Async(Guid id)
var {entityName.ToLower()} = await _context.{entityName}s.FindAsync(id);
if ({entityName.ToLower()} == null)
{{
return NotFound($""{displayName} with ID {{id}} not found"");
return NotFound($""{entityName} with ID {{id}} not found"");
}}
_context.{entityName}s.Remove({entityName.ToLower()});
@ -226,5 +205,3 @@ private async Task<bool> {entityName}ExistsAsync(Guid id)
}}";
}
}

View file

@ -7,21 +7,12 @@ public class DeveloperKitAutoMapperProfile : Profile
{
public DeveloperKitAutoMapperProfile()
{
// SqlTable mappings
CreateMap<SqlTable, SqlTableDto>();
CreateMap<CreateUpdateSqlTableDto, SqlTable>();
// EntityField mappings
CreateMap<SqlTableField, SqlTableFieldDto>();
CreateMap<CreateUpdateSqlTableFieldDto, SqlTableField>();
// CustomComponent mappings
CreateMap<CustomComponent, CustomComponentDto>();
CreateMap<CreateUpdateCustomComponentDto, CustomComponent>();
// GeneratedEndpoint mappings
CreateMap<CrudEndpoint, CrudEndpointDto>()
.ForMember(dest => dest.EntityDisplayName, opt => opt.MapFrom(src => src.Entity != null ? src.Entity.DisplayName : null));
CreateMap<CrudEndpoint, CrudEndpointDto>();
CreateMap<CreateUpdateCrudEndpointDto, CrudEndpoint>();
CreateMap<DynamicService, DynamicServiceDto>()

View file

@ -1,240 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Entities;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.DeveloperKit;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
namespace Platform.Api.Application;
[Authorize]
public class SqlTableAppService : CrudAppService<
SqlTable,
SqlTableDto,
Guid,
PagedAndSortedResultRequestDto,
CreateUpdateSqlTableDto>, ISqlTableAppService
{
private readonly IRepository<SqlTable, Guid> _repository;
private readonly IRepository<SqlTableField, Guid> _fieldRepository;
private readonly IRepository<CrudEndpoint, Guid> _endpointRepository;
public SqlTableAppService(
IRepository<SqlTable, Guid> repository,
IRepository<SqlTableField, Guid> fieldRepository,
IRepository<CrudEndpoint, Guid> endpointRepository) : base(repository)
{
_repository = repository;
_fieldRepository = fieldRepository;
_endpointRepository = endpointRepository;
}
public override async Task<PagedResultDto<SqlTableDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var query = await _repository.GetQueryableAsync();
var fullQuery = query.Include(x => x.Fields.OrderBy(f => f.DisplayOrder));
var totalCount = await fullQuery.CountAsync();
var entities = await fullQuery
.OrderBy(x => x.CreationTime)
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
.ToListAsync();
var dtos = ObjectMapper.Map<List<SqlTable>, List<SqlTableDto>>(entities);
return new PagedResultDto<SqlTableDto>(totalCount, dtos);
}
public override async Task<SqlTableDto> GetAsync(Guid id)
{
var query = await _repository.GetQueryableAsync();
var entity = await query
.Include(x => x.Fields.OrderBy(f => f.DisplayOrder))
.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
throw new EntityNotFoundException($"Sql Table with id {id} not found");
return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
}
public async Task<List<SqlTableDto>> GetActiveEntitiesAsync()
{
var query = await _repository.GetQueryableAsync();
var entities = await query
.Include(x => x.Fields.OrderBy(f => f.DisplayOrder))
.Where(x => x.IsActive)
.ToListAsync();
return ObjectMapper.Map<List<SqlTable>, List<SqlTableDto>>(entities);
}
public async Task<SqlTableDto> ToggleActiveStatusAsync(Guid id)
{
var query = await _repository.GetQueryableAsync();
var entity = await query
.Include(x => x.Fields.OrderBy(f => f.DisplayOrder))
.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
throw new EntityNotFoundException($"Sql Table with id {id} not found");
entity.IsActive = !entity.IsActive;
await _repository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
}
public override async Task<SqlTableDto> UpdateAsync(Guid id, CreateUpdateSqlTableDto input)
{
var query = await _repository.GetQueryableAsync();
var entity = await query
.Include(x => x.Fields)
.FirstOrDefaultAsync(e => e.Id == id);
if (entity == null)
throw new EntityNotFoundException(typeof(SqlTable), id);
entity.Menu = input.Menu;
entity.Name = input.Name;
entity.DisplayName = input.DisplayName;
entity.TableName = input.TableName;
entity.Description = input.Description;
entity.IsActive = input.IsActive;
entity.IsFullAuditedEntity = input.IsFullAuditedEntity;
entity.IsMultiTenant = input.IsMultiTenant;
var updatedFields = new List<SqlTableField>();
for (int i = 0; i < input.Fields.Count; i++)
{
var dtoField = input.Fields[i];
SqlTableField? existingField = null;
if (dtoField.Id.HasValue)
{
existingField = entity.Fields?.FirstOrDefault(f => f.Id == dtoField.Id.Value);
}
if (existingField != null)
{
existingField.Name = dtoField.Name;
existingField.Type = dtoField.Type;
existingField.IsRequired = dtoField.IsRequired;
existingField.MaxLength = dtoField.MaxLength;
existingField.IsUnique = dtoField.IsUnique;
existingField.DefaultValue = dtoField.DefaultValue;
existingField.Description = dtoField.Description;
existingField.DisplayOrder = dtoField.DisplayOrder;
updatedFields.Add(existingField);
}
else
{
var newField = new SqlTableField
{
EntityId = entity.Id,
Name = dtoField.Name,
Type = dtoField.Type,
IsRequired = dtoField.IsRequired,
MaxLength = dtoField.MaxLength,
IsUnique = dtoField.IsUnique,
DefaultValue = dtoField.DefaultValue,
Description = dtoField.Description,
DisplayOrder = dtoField.DisplayOrder
};
await _fieldRepository.InsertAsync(newField);
updatedFields.Add(newField);
}
}
// Silinecek alanlar
var toRemove = entity.Fields?
.Where(existing => updatedFields.All(f => f.Id != existing.Id))
.ToList() ?? [];
if (toRemove.Any())
{
await _fieldRepository.DeleteManyAsync(toRemove);
}
await _repository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
}
public override async Task<SqlTableDto> CreateAsync(CreateUpdateSqlTableDto input)
{
// Entity oluştur
var entity = new SqlTable
{
Menu = input.Menu,
Name = input.Name,
DisplayName = input.DisplayName,
TableName = input.TableName,
Description = input.Description,
IsActive = input.IsActive,
IsFullAuditedEntity = input.IsFullAuditedEntity,
IsMultiTenant = input.IsMultiTenant,
};
// Fields ekle - sıralama ile
for (int i = 0; i < input.Fields.Count; i++)
{
var fieldDto = input.Fields[i];
var field = new SqlTableField
{
EntityId = entity.Id,
Name = fieldDto.Name,
Type = fieldDto.Type,
IsRequired = fieldDto.IsRequired,
MaxLength = fieldDto.MaxLength,
IsUnique = fieldDto.IsUnique,
DefaultValue = fieldDto.DefaultValue,
Description = fieldDto.Description,
DisplayOrder = fieldDto.DisplayOrder
};
entity.Fields.Add(field);
}
entity = await _repository.InsertAsync(entity, autoSave: true);
return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
}
private bool FieldsHaveChanged(ICollection<SqlTableField> existingFields, List<CreateUpdateSqlTableFieldDto> inputFields)
{
if (existingFields.Count != inputFields.Count)
return true;
var existingFieldsList = existingFields.OrderBy(f => f.DisplayOrder).ToList();
var inputFieldsList = inputFields.OrderBy(f => f.DisplayOrder).ToList();
for (int i = 0; i < existingFieldsList.Count; i++)
{
var existing = existingFieldsList[i];
var input = inputFieldsList[i];
if (existing.Name != input.Name ||
existing.Type != input.Type ||
existing.IsRequired != input.IsRequired ||
existing.MaxLength != input.MaxLength ||
existing.IsUnique != input.IsUnique)
{
return true;
}
}
return false;
}
}

View file

@ -29,8 +29,6 @@ public enum TableNameEnum
ForumCategory,
ForumTopic,
ForumPost,
SqlTable,
SqlTableField,
CrudEndpoint,
CustomEndpoint,
CustomComponent,

View file

@ -332,6 +332,8 @@ public static class PlatformConsts
public const string Get = CustomEndpoints + ".Get";
public const string Post = CustomEndpoints + ".Post";
public const string CrudEndpoints = Default + ".CrudEndpoints";
public static class DynamicServices
{
public const string DynamicService = Default + ".DynamicServices";

View file

@ -68,8 +68,6 @@ public static class TableNameResolver
{ nameof(TableNameEnum.Note), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.ReportCategory), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.ReportTemplate), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.SqlTable), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.SqlTableField), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.CrudEndpoint), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.CustomEndpoint), (TablePrefix.TenantByName, MenuPrefix.Administration) },
{ nameof(TableNameEnum.CustomComponent), (TablePrefix.TenantByName, MenuPrefix.Administration) },

View file

@ -15,9 +15,6 @@ public class CrudEndpoint : FullAuditedEntity<Guid>, IMultiTenant
public string CsharpCode { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
// Foreign key to SqlTable
public Guid EntityId { get; set; }
public virtual SqlTable Entity { get; set; } = null!;
public CrudEndpoint()
{
Id = Guid.NewGuid();

View file

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities;
public class SqlTable : FullAuditedEntity<Guid>, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; }
public string Menu { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string TableName { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false;
public string EndpointStatus { get; set; } = "pending"; // "pending" | "applied" | "failed"
public virtual ICollection<SqlTableField> Fields { get; set; } = [];
public SqlTable()
{
Id = Guid.NewGuid(); // Burada erişim mümkün çünkü sınıfın içi
}
}
public class SqlTableField : FullAuditedEntity<Guid>
{
public Guid EntityId { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool IsRequired { get; set; }
public int? MaxLength { get; set; }
public bool IsUnique { get; set; }
public string? DefaultValue { get; set; }
public string? Description { get; set; }
public int DisplayOrder { get; set; } = 0;
public virtual SqlTable Entity { get; set; } = null!;
public SqlTableField()
{
Id = Guid.NewGuid(); // Burada erişim mümkün çünkü sınıfın içi
}
}

View file

@ -1,5 +1,4 @@
using Sozsoft.Platform.Domain.DeveloperKit;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
@ -8,65 +7,59 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore;
public class DynamicEntityManager : IDynamicEntityManager
{
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IRepository<SqlTable, Guid> _sqlTableRepository;
public DynamicEntityManager(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IRepository<SqlTable, Guid> sqlTableRepository)
IDbContextProvider<PlatformDbContext> dbContextProvider)
{
_dbContextProvider = dbContextProvider;
_sqlTableRepository = sqlTableRepository;
}
public async Task<List<object>?> GetEntityListAsync(string entityName)
{
return await ExecuteDynamicQuery(entityName, "GetList", null);
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var query = hasIsDeleted
? $"SELECT * FROM [{entityName}] WHERE IsDeleted = 0 OR IsDeleted IS NULL"
: $"SELECT * FROM [{entityName}]";
return await ExecuteRawQueryAsync(query);
}
public async Task<object?> GetEntityByIdAsync(string entityName, Guid id)
{
var result = await ExecuteDynamicQuery(entityName, "GetById", id);
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var query = hasIsDeleted
? $"SELECT * FROM [{entityName}] WHERE Id = '{id}' AND (IsDeleted = 0 OR IsDeleted IS NULL)"
: $"SELECT * FROM [{entityName}] WHERE Id = '{id}'";
var result = await ExecuteRawQueryAsync(query);
return result?.FirstOrDefault();
}
public async Task<object?> CreateEntityAsync(string entityName, JsonElement data)
{
var entity = await GetEntityDefinitionAsync(entityName);
var tableName = entity.TableName;
var newId = Guid.NewGuid();
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
var hasCreationTime = await ColumnExistsAsync(entityName, "CreationTime");
var columns = new List<string> { "Id" };
var columns = new List<string> { "[Id]" };
var values = new List<string> { $"'{newId}'" };
foreach (var field in entity.Fields)
if (hasCreationTime) { columns.Add("[CreationTime]"); values.Add("SYSUTCDATETIME()"); }
if (hasIsDeleted) { columns.Add("[IsDeleted]"); values.Add("0"); }
foreach (var prop in data.EnumerateObject())
{
if (data.TryGetProperty(field.Name, out var fieldValue))
{
columns.Add($"[{field.Name}]");
values.Add(FormatValueForSql(fieldValue, field.Type));
}
if (prop.NameEquals("id") || prop.NameEquals("Id"))
continue;
columns.Add($"[{prop.Name}]");
values.Add(FormatValueForSql(prop.Value));
}
if (entity.IsMultiTenant && data.TryGetProperty("TenantId", out var tenantIdValue))
{
columns.Add("[TenantId]");
values.Add($"'{tenantIdValue.GetGuid()}'");
}
if (entity.IsFullAuditedEntity)
{
columns.AddRange(new[] { "[CreationTime]", "[LastModificationTime]", "[IsDeleted]" });
values.AddRange(new[] { "SYSUTCDATETIME()", "NULL", "0" });
}
var insertQuery = $"INSERT INTO [{tableName}] ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
var insertQuery = $"INSERT INTO [{entityName}] ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
var dbContext = await _dbContextProvider.GetDbContextAsync();
await dbContext.Database.ExecuteSqlRawAsync(insertQuery);
@ -76,26 +69,25 @@ public class DynamicEntityManager : IDynamicEntityManager
public async Task<object?> UpdateEntityAsync(string entityName, Guid id, JsonElement data)
{
var entity = await GetEntityDefinitionAsync(entityName);
var tableName = entity.TableName;
var existing = await GetEntityByIdAsync(entityName, id);
if (existing == null)
return null;
var setParts = new List<string>();
foreach (var field in entity.Fields)
{
if (data.TryGetProperty(field.Name, out var fieldValue))
{
setParts.Add($"[{field.Name}] = {FormatValueForSql(fieldValue, field.Type)}");
}
}
if (entity.IsFullAuditedEntity)
var hasLastModification = await ColumnExistsAsync(entityName, "LastModificationTime");
if (hasLastModification)
setParts.Add("[LastModificationTime] = SYSUTCDATETIME()");
var updateQuery = $"UPDATE [{tableName}] SET {string.Join(", ", setParts)} WHERE Id = '{id}'";
foreach (var prop in data.EnumerateObject())
{
if (prop.NameEquals("id") || prop.NameEquals("Id"))
continue;
setParts.Add($"[{prop.Name}] = {FormatValueForSql(prop.Value)}");
}
var updateQuery = $"UPDATE [{entityName}] SET {string.Join(", ", setParts)} WHERE Id = '{id}'";
var dbContext = await _dbContextProvider.GetDbContextAsync();
await dbContext.Database.ExecuteSqlRawAsync(updateQuery);
@ -105,37 +97,61 @@ public class DynamicEntityManager : IDynamicEntityManager
public async Task<bool> DeleteEntityAsync(string entityName, Guid id)
{
var entity = await GetEntityDefinitionAsync(entityName);
var tableName = entity.TableName;
var existing = await GetEntityByIdAsync(entityName, id);
if (existing == null)
return false;
string deleteQuery;
if (entity.IsFullAuditedEntity)
{
// Full audited entities always have soft delete
deleteQuery = $"UPDATE [{tableName}] SET [IsDeleted] = 1, [DeletionTime] = SYSUTCDATETIME() WHERE Id = '{id}'";
}
else
{
deleteQuery = $"DELETE FROM [{tableName}] WHERE Id = '{id}'";
}
var dbContext = await _dbContextProvider.GetDbContextAsync();
var affected = await dbContext.Database.ExecuteSqlRawAsync(deleteQuery);
return affected > 0;
var hasIsDeleted = await ColumnExistsAsync(entityName, "IsDeleted");
try
{
if (hasIsDeleted)
{
var hasDeletionTime = await ColumnExistsAsync(entityName, "DeletionTime");
var softDeleteQuery = hasDeletionTime
? $"UPDATE [{entityName}] SET [IsDeleted] = 1, [DeletionTime] = SYSUTCDATETIME() WHERE Id = '{id}'"
: $"UPDATE [{entityName}] SET [IsDeleted] = 1 WHERE Id = '{id}'";
var affected = await dbContext.Database.ExecuteSqlRawAsync(softDeleteQuery);
return affected > 0;
}
else
{
var hardDeleteQuery = $"DELETE FROM [{entityName}] WHERE Id = '{id}'";
var affected = await dbContext.Database.ExecuteSqlRawAsync(hardDeleteQuery);
return affected > 0;
}
}
catch
{
var hardDeleteQuery = $"DELETE FROM [{entityName}] WHERE Id = '{id}'";
var affected = await dbContext.Database.ExecuteSqlRawAsync(hardDeleteQuery);
return affected > 0;
}
}
private async Task<List<object>> ExecuteDynamicQuery(string entityName, string operation, Guid? id)
private async Task<bool> ColumnExistsAsync(string tableName, string columnName)
{
var entity = await GetEntityDefinitionAsync(entityName);
var tableName = entity.TableName;
var query = operation == "GetList"
? $"SELECT * FROM [{tableName}]"
: $"SELECT * FROM [{tableName}] WHERE Id = '{id}'";
var query = $"SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '{tableName}' AND COLUMN_NAME = '{columnName}'";
var dbContext = await _dbContextProvider.GetDbContextAsync();
var connection = dbContext.Database.GetDbConnection();
await dbContext.Database.OpenConnectionAsync();
try
{
using var command = connection.CreateCommand();
command.CommandText = query;
command.Transaction = dbContext.Database.CurrentTransaction?.GetDbTransaction();
var result = await command.ExecuteScalarAsync();
return Convert.ToInt32(result) > 0;
}
finally
{
await dbContext.Database.CloseConnectionAsync();
}
}
private async Task<List<object>> ExecuteRawQueryAsync(string query)
{
var dbContext = await _dbContextProvider.GetDbContextAsync();
var connection = dbContext.Database.GetDbConnection();
await dbContext.Database.OpenConnectionAsync();
@ -144,8 +160,6 @@ public class DynamicEntityManager : IDynamicEntityManager
{
using var command = connection.CreateCommand();
command.CommandText = query;
// ⭐️⭐️⭐️ Kritik satır:
command.Transaction = dbContext.Database.CurrentTransaction?.GetDbTransaction();
using var reader = await command.ExecuteReaderAsync();
@ -167,25 +181,19 @@ public class DynamicEntityManager : IDynamicEntityManager
}
}
private async Task<SqlTable> GetEntityDefinitionAsync(string entityName)
private static string FormatValueForSql(JsonElement value)
{
var queryable = await _sqlTableRepository.WithDetailsAsync(x => x.Fields);
var entity = await queryable.FirstOrDefaultAsync(x => x.Name.ToLower() == entityName.ToLower());
return entity ?? throw new UserFriendlyException($"Entity '{entityName}' not found.");
}
private static string FormatValueForSql(JsonElement value, string type)
{
return type.ToLower() switch
return value.ValueKind switch
{
"string" => $"'{value.GetString()?.Replace("'", "''")}'",
"number" => value.GetInt32().ToString(),
"decimal" => value.GetDecimal().ToString(System.Globalization.CultureInfo.InvariantCulture),
"boolean" => value.GetBoolean() ? "1" : "0",
"date" => $"'{value.GetDateTime():yyyy-MM-dd HH:mm:ss}'",
"guid" => $"'{value.GetGuid()}'",
_ => $"'{value.GetString()?.Replace("'", "''")}'",
JsonValueKind.Number when value.TryGetInt64(out var l) => l.ToString(),
JsonValueKind.Number when value.TryGetDecimal(out var d) => d.ToString(System.Globalization.CultureInfo.InvariantCulture),
JsonValueKind.True => "1",
JsonValueKind.False => "0",
JsonValueKind.Null => "NULL",
JsonValueKind.String when value.TryGetGuid(out var g) => $"'{g}'",
JsonValueKind.String when value.TryGetDateTime(out var dt) => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
JsonValueKind.String => $"'{value.GetString()?.Replace("'", "''")}'" ,
_ => "NULL",
};
}
}

View file

@ -58,8 +58,6 @@ public class PlatformDbContext :
public DbSet<ForumCategory> ForumCategories { get; set; }
public DbSet<ForumTopic> ForumTopics { get; set; }
public DbSet<ForumPost> ForumPosts { get; set; }
public DbSet<SqlTable> CustomEntities { get; set; }
public DbSet<SqlTableField> EntityFields { get; set; }
public DbSet<CrudEndpoint> GeneratedEndpoints { get; set; }
public DbSet<CustomEndpoint> CustomEndpoints { get; set; }
public DbSet<CustomComponent> CustomComponents { get; set; }
@ -515,37 +513,6 @@ public class PlatformDbContext :
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<SqlTable>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SqlTable)), Prefix.DbSchema);
b.ConfigureByConvention();
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.DisplayName).IsRequired().HasMaxLength(128);
b.Property(x => x.TableName).IsRequired().HasMaxLength(128);
b.Property(x => x.Description).HasMaxLength(512);
b.Property(x => x.EndpointStatus).IsRequired().HasMaxLength(64);
b.Property(x => x.Menu).IsRequired().HasMaxLength(128);
b.HasMany(x => x.Fields)
.WithOne(x => x.Entity)
.HasForeignKey(x => x.EntityId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<SqlTableField>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SqlTableField)), Prefix.DbSchema);
b.ConfigureByConvention();
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.Type).IsRequired().HasMaxLength(64);
b.Property(x => x.Description).HasMaxLength(512);
b.Property(x => x.DefaultValue).HasMaxLength(256);
});
builder.Entity<CrudEndpoint>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CrudEndpoint)), Prefix.DbSchema);
@ -557,11 +524,6 @@ public class PlatformDbContext :
b.Property(x => x.Path).IsRequired().HasMaxLength(256);
b.Property(x => x.OperationType).IsRequired().HasMaxLength(64);
b.Property(x => x.CsharpCode).IsRequired();
b.HasOne(x => x.Entity)
.WithMany()
.HasForeignKey(x => x.EntityId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<CustomEndpoint>(b =>

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260302174517_Initial")]
[Migration("20260302191746_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -1368,9 +1368,6 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
@ -1414,8 +1411,6 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Adm_T_CrudEndpoint", (string)null);
});
@ -3772,163 +3767,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_SkillType", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EndpointStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Menu")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("TableName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Adm_T_SqlTable", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DefaultValue")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<bool>("IsUnique")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxLength")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Adm_T_SqlTableField", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{
b.Property<string>("Id")
@ -6402,17 +6240,6 @@ namespace Sozsoft.Platform.Migrations
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudEndpoint", b =>
{
b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany()
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.District", b =>
{
b.HasOne("Sozsoft.Platform.Entities.City", null)
@ -6503,17 +6330,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("SkillType");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany("Fields")
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{
b.HasOne("Sozsoft.Platform.Entities.UomCategory", "UomCategory")
@ -6753,11 +6569,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Skills");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Navigation("Fields");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.UomCategory", b =>
{
b.Navigation("Uoms");

View file

@ -591,6 +591,31 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Adm_T_Contact", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Adm_T_CrudEndpoint",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Method = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
Path = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
OperationType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CsharpCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Adm_T_CrudEndpoint", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Adm_T_CustomComponent",
columns: table => new
@ -986,34 +1011,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Adm_T_SkillType", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Adm_T_SqlTable",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Menu = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
TableName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsFullAuditedEntity = table.Column<bool>(type: "bit", nullable: false),
IsMultiTenant = table.Column<bool>(type: "bit", nullable: false),
EndpointStatus = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Adm_T_SqlTable", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Adm_T_UomCategory",
columns: table => new
@ -1945,71 +1942,6 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Adm_T_CrudEndpoint",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Method = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
Path = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
OperationType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CsharpCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Adm_T_CrudEndpoint", x => x.Id);
table.ForeignKey(
name: "FK_Adm_T_CrudEndpoint_Adm_T_SqlTable_EntityId",
column: x => x.EntityId,
principalTable: "Adm_T_SqlTable",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Adm_T_SqlTableField",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
IsRequired = table.Column<bool>(type: "bit", nullable: false),
MaxLength = table.Column<int>(type: "int", nullable: true),
IsUnique = table.Column<bool>(type: "bit", nullable: false),
DefaultValue = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
DisplayOrder = table.Column<int>(type: "int", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Adm_T_SqlTableField", x => x.Id);
table.ForeignKey(
name: "FK_Adm_T_SqlTableField_Adm_T_SqlTable_EntityId",
column: x => x.EntityId,
principalTable: "Adm_T_SqlTable",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Adm_T_Uom",
columns: table => new
@ -2840,11 +2772,6 @@ namespace Sozsoft.Platform.Migrations
table: "Adm_T_BlogPost",
column: "Slug");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_CrudEndpoint_EntityId",
table: "Adm_T_CrudEndpoint",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_OrderItem_OrderId",
table: "Adm_T_OrderItem",
@ -2871,11 +2798,6 @@ namespace Sozsoft.Platform.Migrations
table: "Adm_T_SkillLevel",
column: "SkillTypeId");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_SqlTableField_EntityId",
table: "Adm_T_SqlTableField",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_Adm_T_Uom_UomCategoryId",
table: "Adm_T_Uom",
@ -3151,9 +3073,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Adm_T_SkillLevel");
migrationBuilder.DropTable(
name: "Adm_T_SqlTableField");
migrationBuilder.DropTable(
name: "Adm_T_Uom");
@ -3238,9 +3157,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Adm_T_SkillType");
migrationBuilder.DropTable(
name: "Adm_T_SqlTable");
migrationBuilder.DropTable(
name: "Adm_T_UomCategory");

View file

@ -1365,9 +1365,6 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
@ -1411,8 +1408,6 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Adm_T_CrudEndpoint", (string)null);
});
@ -3769,163 +3764,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_SkillType", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EndpointStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Menu")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("TableName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Adm_T_SqlTable", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DefaultValue")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<bool>("IsUnique")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxLength")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Adm_T_SqlTableField", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{
b.Property<string>("Id")
@ -6399,17 +6237,6 @@ namespace Sozsoft.Platform.Migrations
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudEndpoint", b =>
{
b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany()
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.District", b =>
{
b.HasOne("Sozsoft.Platform.Entities.City", null)
@ -6500,17 +6327,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("SkillType");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany("Fields")
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{
b.HasOne("Sozsoft.Platform.Entities.UomCategory", "UomCategory")
@ -6750,11 +6566,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Skills");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Navigation("Fields");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.UomCategory", b =>
{
b.Navigation("Uoms");

View file

@ -1,212 +0,0 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import { developerKitService } from "@/services/developerKit.service";
import {
CrudEndpoint,
CreateUpdateSqlTableDto,
SqlTable,
} from "@/proxy/developerKit/models";
export const FIELD_TYPE_OPTIONS = [
{ label: "String", value: "string" },
{ label: "Number", value: "number" },
{ label: "Boolean", value: "boolean" },
{ label: "Date", value: "date" },
{ label: "Guid", value: "guid" },
{ label: "Decimal", value: "decimal" },
] as const;
export type EntityFieldType = (typeof FIELD_TYPE_OPTIONS)[number]["value"];
interface EntityContextType {
entities: SqlTable[];
generatedEndpoints: CrudEndpoint[];
loading: boolean;
error: string | null;
// Entity operations
addEntity: (entity: CreateUpdateSqlTableDto) => Promise<void>;
updateEntity: (id: string, entity: CreateUpdateSqlTableDto) => Promise<void>;
deleteEntity: (id: string) => Promise<void>;
getEntity: (id: string) => SqlTable | undefined;
refreshEntities: () => Promise<void>;
toggleEntityActiveStatus: (id: string) => Promise<void>;
// Generated endpoint operations
generateCrudEndpoints: (entityId: string) => Promise<void>;
toggleEndpoint: (endpointId: string) => Promise<void>;
getEntityEndpoints: (entityId: string) => CrudEndpoint[];
deleteGeneratedEndpoint: (id: string) => Promise<void>;
}
const EntityContext = createContext<EntityContextType | undefined>(undefined);
// eslint-disable-next-line react-refresh/only-export-components
export const useEntities = () => {
const context = useContext(EntityContext);
if (context === undefined) {
throw new Error("useEntities must be used within an EntityProvider");
}
return context;
};
export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [entities, setEntities] = useState<SqlTable[]>([]);
const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const refreshEntities = async () => {
try {
setLoading(true);
setError(null);
const [entitiesData, generatedEndpointsData] = await Promise.all([
developerKitService.getSqlTables(),
developerKitService.getGeneratedEndpoints(),
]);
setEntities(entitiesData.items || []);
setGeneratedEndpoints(generatedEndpointsData.items || []);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch data");
console.error("Failed to fetch data:", err);
} finally {
setLoading(false);
}
};
useEffect(() => {
refreshEntities();
}, []);
const addEntity = async (entityData: CreateUpdateSqlTableDto) => {
try {
setLoading(true);
setError(null);
const newEntity = await developerKitService.createSqlTable(entityData);
setEntities((prev) => [...prev, newEntity]);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create entity");
throw err;
} finally {
setLoading(false);
}
};
const updateEntity = async (id: string, entityData: CreateUpdateSqlTableDto) => {
try {
setLoading(true);
setError(null);
const updatedEntity = await developerKitService.updateSqlTable(id, entityData);
setEntities((prev) => prev.map((e) => (e.id === id ? updatedEntity : e)));
await refreshEntities();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to update entity");
throw err;
} finally {
setLoading(false);
}
};
const deleteEntity = async (id: string) => {
try {
setLoading(true);
setError(null);
await developerKitService.deleteSqlTable(id);
setEntities((prev) => prev.filter((e) => e.id !== id));
setGeneratedEndpoints((prev) => prev.filter((ep) => ep.entityId !== id));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete entity");
throw err;
} finally {
setLoading(false);
}
};
const toggleEntityActiveStatus = async (id: string) => {
try {
setLoading(true);
setError(null);
await developerKitService.toggleSqlTableActiveStatus(id);
await refreshEntities();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to toggle entity status");
throw err;
} finally {
setLoading(false);
}
};
const getEntity = (id: string): SqlTable | undefined =>
entities.find((e) => e.id === id);
const generateCrudEndpoints = async (entityId: string) => {
try {
setLoading(true);
setError(null);
const endpointsData = await developerKitService.generateCrudEndpoints(entityId);
setGeneratedEndpoints((prev) => [
...prev.filter((e) => e.entityId !== entityId),
...(endpointsData.items || []),
]);
await refreshEntities();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to generate CRUD endpoints");
throw err;
} finally {
setLoading(false);
}
};
const toggleEndpoint = async (endpointId: string) => {
try {
setLoading(true);
setError(null);
const updatedEndpoint = await developerKitService.toggleGeneratedEndpoint(endpointId);
setGeneratedEndpoints((prev) =>
prev.map((e) => (e.id === endpointId ? updatedEndpoint : e)),
);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to toggle endpoint");
throw err;
} finally {
setLoading(false);
}
};
const getEntityEndpoints = (entityId: string): CrudEndpoint[] =>
generatedEndpoints.filter((e) => e.entityId === entityId);
const deleteGeneratedEndpoint = async (id: string) => {
try {
setLoading(true);
setError(null);
await developerKitService.deleteGeneratedEndpoint(id);
setGeneratedEndpoints((prev) => prev.filter((e) => e.id !== id));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete generated endpoint");
throw err;
} finally {
setLoading(false);
}
};
return (
<EntityContext.Provider
value={{
entities,
generatedEndpoints,
loading,
error,
addEntity,
updateEntity,
deleteEntity,
getEntity,
refreshEntities,
toggleEntityActiveStatus,
generateCrudEndpoints,
toggleEndpoint,
getEntityEndpoints,
deleteGeneratedEndpoint,
}}
>
{children}
</EntityContext.Provider>
);
};

View file

@ -1,61 +1,7 @@
export interface SqlTable {
id: string;
menu: string;
name: string;
displayName: string;
tableName: string;
description?: string;
isActive: boolean;
isFullAuditedEntity: boolean;
isMultiTenant: boolean;
endpointStatus: "pending" | "applied" | "failed";
fields: SqlTableField[];
creationTime: string;
lastModificationTime?: string;
}
export interface SqlTableField {
id: string;
entityId: string;
name: string;
type: "string" | "number" | "boolean" | "date" | "guid" | "decimal";
isRequired: boolean;
maxLength?: number;
isUnique?: boolean;
defaultValue?: string;
description?: string;
creationTime?: string;
lastModificationTime?: string;
displayOrder: number;
}
export interface CreateUpdateSqlTableFieldDto {
id?: string;
name: string;
type: "string" | "number" | "boolean" | "date" | "guid" | "decimal";
isRequired: boolean;
maxLength?: number;
isUnique?: boolean;
defaultValue?: string;
description?: string;
displayOrder: number;
}
export interface CreateUpdateSqlTableDto {
menu: string;
name: string;
displayName: string;
tableName: string;
description?: string;
isActive: boolean;
isFullAuditedEntity: boolean;
isMultiTenant: boolean;
fields: CreateUpdateSqlTableFieldDto[];
}
export interface CrudEndpoint {
id: string;
entityId: string;
tenantId: string;
entityName: string;
method: "GET" | "POST" | "PUT" | "DELETE";
path: string;
@ -67,7 +13,7 @@ export interface CrudEndpoint {
}
export interface CreateUpdateCrudEndpointDto {
entityId: string;
tenantId: string;
entityName: string;
method: "GET" | "POST" | "PUT" | "DELETE";
path: string;
@ -78,6 +24,7 @@ export interface CreateUpdateCrudEndpointDto {
export interface CustomComponent {
id: string;
tenantId?: string;
name: string;
code: string;
props?: string;
@ -90,6 +37,7 @@ export interface CustomComponent {
export interface CustomComponentDto {
id: string;
tenantId?: string;
name: string;
code: string;
props?: string;
@ -101,10 +49,28 @@ export interface CustomComponentDto {
}
export interface CreateUpdateCustomComponentDto {
tenantId?: string;
name: string;
code: string;
props?: string;
description?: string;
isActive: boolean;
dependencies?: string;
}
export type RelationshipType = 'OneToOne' | 'OneToMany'
export type CascadeBehavior = 'NoAction' | 'Cascade' | 'SetNull' | 'Restrict'
export interface SqlTableRelation {
id: string
/** Existing DB constraint name — set when loaded from database (edit mode) */
constraintName?: string
relationshipType: RelationshipType
fkColumnName: string
referencedTable: string
referencedColumn: string
cascadeDelete: CascadeBehavior
cascadeUpdate: CascadeBehavior
isRequired: boolean
description: string
}

View file

@ -4,69 +4,11 @@ import {
CrudEndpoint,
CreateUpdateCrudEndpointDto,
CreateUpdateCustomComponentDto,
CreateUpdateSqlTableDto,
CustomComponent,
CustomComponentDto,
SqlTable,
} from '@/proxy/developerKit/models'
class DeveloperKitService {
async getSqlTables(): Promise<PagedResultDto<SqlTable>> {
const response = await apiService.fetchData<PagedResultDto<SqlTable>>({
url: '/api/app/sql-table',
method: 'GET',
})
return response.data
}
async getActiveSqlTables(): Promise<PagedResultDto<SqlTable>> {
const response = await apiService.fetchData<PagedResultDto<SqlTable>>({
url: '/api/app/sql-table/active-tables',
method: 'GET',
})
return response.data
}
async getSqlTable(id: string): Promise<SqlTable> {
const response = await apiService.fetchData<SqlTable>({
url: `/api/app/sql-table/${id}`,
method: 'GET',
})
return response.data
}
async createSqlTable(data: CreateUpdateSqlTableDto): Promise<SqlTable> {
const response = await apiService.fetchData<SqlTable>({
url: '/api/app/sql-table',
method: 'POST',
data: data as any,
})
return response.data
}
async updateSqlTable(id: string, entity: CreateUpdateSqlTableDto): Promise<SqlTable> {
const response = await apiService.fetchData<SqlTable>({
url: `/api/app/sql-table/${id}`,
method: 'PUT',
data: entity as any,
})
return response.data
}
async deleteSqlTable(id: string): Promise<void> {
await apiService.fetchData<void>({
url: `/api/app/sql-table/${id}`,
method: 'DELETE',
})
}
async toggleSqlTableActiveStatus(id: string): Promise<void> {
await apiService.fetchData<void>({
url: `/api/app/sql-table/${id}/toggle-active-status`,
method: 'POST',
})
}
// Custom Component endpoints
async getCustomComponents(): Promise<PagedResultDto<CustomComponentDto>> {
const response = await apiService.fetchData<PagedResultDto<CustomComponentDto>>({
@ -121,14 +63,6 @@ class DeveloperKitService {
}
// Generated Endpoint endpoints
async getGeneratedEndpoints(): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: '/api/app/crud-endpoint-generate',
method: 'GET',
})
return response.data
}
async getActiveGeneratedEndpoints(): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: '/api/app/crud-endpoint-generate/active-endpoints',
@ -137,9 +71,25 @@ class DeveloperKitService {
return response.data
}
async getEndpointsByEntity(entityId: string): Promise<PagedResultDto<CrudEndpoint>> {
async toggleGeneratedEndpoint(id: string): Promise<CrudEndpoint> {
const response = await apiService.fetchData<CrudEndpoint>({
url: `/api/app/crud-endpoint-generate/${id}/toggle`,
method: 'POST',
})
return response.data
}
async generateCrudEndpoints(entityName: string): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: `/api/app/crud-endpoint-generate/endpoints-by-entity/${entityId}`,
url: `/api/app/crud-endpoint-generate/generate-crud-endpoints/${entityName}`,
method: 'POST',
})
return response.data
}
async getGeneratedListEndpoints(): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: '/api/app/crud-endpoint-generate',
method: 'GET',
})
return response.data
@ -180,38 +130,6 @@ class DeveloperKitService {
method: 'DELETE',
})
}
async toggleGeneratedEndpoint(id: string): Promise<CrudEndpoint> {
const response = await apiService.fetchData<CrudEndpoint>({
url: `/api/app/crud-endpoint-generate/${id}/toggle`,
method: 'POST',
})
return response.data
}
async generateCrudEndpoints(entityId: string): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: `/api/app/crud-endpoint-generate/generate-crud-endpoints/${entityId}`,
method: 'POST',
})
return response.data
}
// Health Check
async healthCheck(): Promise<{ status: string; timestamp: string }> {
try {
const response = await apiService.fetchData({
url: '/api/app/health',
method: 'GET',
})
if (response.status === 200) {
return { status: 'healthy', timestamp: new Date().toISOString() }
}
return { status: 'unhealthy', timestamp: new Date().toISOString() }
} catch {
return { status: 'offline', timestamp: new Date().toISOString() }
}
}
}
export const developerKitService = new DeveloperKitService()

View file

@ -24,7 +24,7 @@ import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { developerKitService } from '@/services/developerKit.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
import type { CrudEndpoint, SqlTable } from '@/proxy/developerKit/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
@ -62,25 +62,14 @@ const METHOD_COLOR: Record<string, string> = {
const CrudEndpointManager: React.FC = () => {
const { translate } = useLocalization()
// Local entity + endpoint state (no context dependency)
const [entities, setEntities] = useState<SqlTable[]>([])
// Endpoint state
const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([])
const refreshData = useCallback(async () => {
try {
const [entitiesRes, endpointsRes] = await Promise.all([
developerKitService.getSqlTables(),
developerKitService.getGeneratedEndpoints(),
])
setEntities(entitiesRes.items || [])
setGeneratedEndpoints(endpointsRes.items || [])
} catch (err) {
console.error('Failed to refresh data', err)
}
}, [])
useEffect(() => {
refreshData()
developerKitService
.getGeneratedListEndpoints()
.then((res) => setGeneratedEndpoints(res.items || []))
.catch((err) => console.error('Failed to load endpoints', err))
}, [])
// Data source + tables
@ -135,19 +124,12 @@ const CrudEndpointManager: React.FC = () => {
}, [selectedDataSource])
// Helpers
const getEntityForTable = useCallback(
(tableName: string): SqlTable | undefined =>
entities.find((e) => e.tableName.toLowerCase() === tableName.toLowerCase()),
[entities],
)
const getEndpointsForTable = useCallback(
(tableName: string): CrudEndpoint[] => {
const entity = getEntityForTable(tableName)
if (!entity) return []
return generatedEndpoints.filter((ep) => ep.entityId === entity.id)
const entityName = toPascalCase(tableName)
return generatedEndpoints.filter((ep) => ep.entityName === entityName)
},
[entities, generatedEndpoints, getEntityForTable],
[generatedEndpoints],
)
const activeEndpointCount = (tableName: string) =>
@ -181,30 +163,10 @@ const CrudEndpointManager: React.FC = () => {
const key = table.fullName
setGeneratingFor(key)
try {
let entity = getEntityForTable(table.tableName)
if (!entity) {
const entityName = toPascalCase(table.tableName)
await developerKitService.createSqlTable({
menu: 'auto',
name: entityName,
displayName: entityName,
tableName: table.tableName,
description: `Auto-created from ${table.fullName}`,
isActive: true,
isFullAuditedEntity: false,
isMultiTenant: false,
fields: [],
})
const fresh = await developerKitService.getSqlTables()
setEntities(fresh.items || [])
entity = (fresh.items || []).find(
(e) => e.tableName.toLowerCase() === table.tableName.toLowerCase(),
)
}
if (!entity) throw new Error('Entity could not be created')
const result = await developerKitService.generateCrudEndpoints(entity.id)
const entityName = toPascalCase(table.tableName)
const result = await developerKitService.generateCrudEndpoints(entityName)
setGeneratedEndpoints((prev) => [
...prev.filter((ep) => ep.entityId !== entity!.id),
...prev.filter((ep) => ep.entityName !== entityName),
...(result.items || []),
])
} catch (err) {
@ -352,7 +314,6 @@ const CrudEndpointManager: React.FC = () => {
const tablesWithEndpoints = dbTables.filter((t) => allEndpointCount(t.tableName) > 0).length
const totalActiveEndpoints = generatedEndpoints.filter((ep) => ep.isActive).length
const selectedTableEndpoints = selectedTable ? getEndpointsForTable(selectedTable.tableName) : []
const selectedEntity = selectedTable ? getEntityForTable(selectedTable.tableName) : undefined
return (
<div className="flex flex-col h-full gap-4">
@ -553,11 +514,6 @@ const CrudEndpointManager: React.FC = () => {
<div>
<h4 className="text-slate-900">{selectedTable.schemaName}.{selectedTable.tableName}</h4>
</div>
{selectedEntity && (
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
Entity: {selectedEntity.name}
</span>
)}
</div>
<div className="flex items-center gap-2">
{selectedTableEndpoints.length > 0 && (
@ -598,9 +554,7 @@ const CrudEndpointManager: React.FC = () => {
<FaBolt className="text-3xl mb-3 text-slate-200" />
<p className="font-medium">Henuz endpoint yok</p>
<p className="text-sm mt-1">
{selectedEntity
? '"CRUD Endpoint Olustur" butonuna tiklayin'
: 'Bu tablo icin Sql Table otomatik olusturulacak ve 5 endpoint uretilecektir'}
"CRUD Endpoint Olustur" butonuna tiklayin
</p>
</div>
) : (
@ -655,22 +609,6 @@ const CrudEndpointManager: React.FC = () => {
</code>
</div>
{/* Status */}
{ep.isActive ? (
<span className="text-xs text-green-600 font-medium">Aktif</span>
) : (
<span className="text-xs text-slate-400">Devre disi</span>
)}
{/* Copy path */}
<button
onClick={() => navigator.clipboard.writeText(ep.path)}
className="p-1.5 text-slate-400 hover:text-slate-700 transition-colors"
title="Path kopyala"
>
<FaCopy className="text-xs" />
</button>
{/* Expand */}
<button
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}

View file

@ -1,4 +1,5 @@
import { useState, useCallback, useMemo, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui'
import {
FaPlus,
@ -8,7 +9,6 @@ import {
FaTable,
FaCloudUploadAlt,
FaCheck,
FaChevronRight,
FaLink,
FaEdit,
FaTimes,
@ -16,9 +16,8 @@ import {
} from 'react-icons/fa'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { MenuService } from '@/services/menu.service'
import { developerKitService } from '@/services/developerKit.service'
import type { CreateUpdateSqlTableDto, CreateUpdateSqlTableFieldDto } from '@/proxy/developerKit/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models'
// ─── Types ────────────────────────────────────────────────────────────────────
@ -55,11 +54,6 @@ interface TableSettings {
menuPrefix: string
entityName: string
tableName: string
displayName: string
description: string
isActive: boolean
isFullAudited: boolean
isMultiTenant: boolean
}
interface TableDesignerDialogProps {
@ -70,23 +64,6 @@ interface TableDesignerDialogProps {
initialTableData?: { schemaName: string; tableName: string } | null
}
type RelationshipType = 'OneToOne' | 'OneToMany'
type CascadeBehavior = 'NoAction' | 'Cascade' | 'SetNull' | 'Restrict'
interface FkRelationshipDefinition {
id: string
/** Existing DB constraint name — set when loaded from database (edit mode) */
constraintName?: string
relationshipType: RelationshipType
fkColumnName: string
referencedTable: string
referencedColumn: string
cascadeDelete: CascadeBehavior
cascadeUpdate: CascadeBehavior
isRequired: boolean
description: string
}
// ─── Constants ────────────────────────────────────────────────────────────────
const DATA_TYPES: { value: SqlDataType; label: string }[] = [
@ -104,6 +81,15 @@ const DATA_TYPES: { value: SqlDataType; label: string }[] = [
]
const FULL_AUDIT_COLUMNS: ColumnDefinition[] = [
{
id: '__Id',
columnName: 'Id',
dataType: 'uniqueidentifier',
maxLength: '',
isNullable: false,
defaultValue: 'NEWID()',
description: 'Primary key',
},
{
id: '__CreationTime',
columnName: 'CreationTime',
@ -181,13 +167,13 @@ const CASCADE_OPTIONS: { value: CascadeBehavior; label: string }[] = [
{ value: 'Restrict', label: 'Restrict' },
]
const EMPTY_FK: Omit<FkRelationshipDefinition, 'id'> = {
const EMPTY_FK: Omit<SqlTableRelation, 'id'> = {
relationshipType: 'OneToMany',
fkColumnName: '',
referencedTable: '',
referencedColumn: 'Id',
cascadeDelete: 'NoAction',
cascadeUpdate: 'NoAction',
cascadeUpdate: 'Cascade',
isRequired: false,
description: '',
}
@ -227,21 +213,17 @@ function colToSqlLine(col: ColumnDefinition, addComma = true): string {
function generateCreateTableSql(
columns: ColumnDefinition[],
settings: TableSettings,
relationships: FkRelationshipDefinition[],
relationships: SqlTableRelation[],
): string {
const tableName = settings.tableName || 'NewTable'
const fullTableName = `[dbo].[${tableName}]`
const userCols = columns.filter((c) => c.columnName.trim())
const auditCols = settings.isFullAudited ? FULL_AUDIT_COLUMNS : []
const tenantCols = settings.isMultiTenant ? [TENANT_COLUMN] : []
const allBodyCols = [...userCols, ...auditCols, ...tenantCols]
const allBodyCols = userCols
// When Full Audited: auto-add Id (PK) + audit cols
// When not Full Audited: user is responsible for their own Id column
const bodyLines = settings.isFullAudited
const hasIdCol = userCols.some((c) => c.columnName.trim().toLowerCase() === 'id')
const bodyLines = hasIdCol
? [
` [Id] uniqueidentifier NOT NULL DEFAULT NEWID(),`,
...allBodyCols.map((c) => colToSqlLine(c, true)),
` CONSTRAINT [PK_${tableName}] PRIMARY KEY NONCLUSTERED ([Id])`,
]
@ -266,8 +248,6 @@ function generateCreateTableSql(
const lines: string[] = [
`/* ── Table: ${fullTableName} ── */`,
...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []),
...(settings.isFullAudited ? ['/* Full Audited Entity (Id + audit columns auto-added) */'] : []),
...(settings.isMultiTenant ? ['/* Multi-Tenant */'] : []),
...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []),
'',
`CREATE TABLE ${fullTableName}`,
@ -334,8 +314,8 @@ function generateAlterTableSql(
originalCols: ColumnDefinition[],
currentCols: ColumnDefinition[],
tableName: string,
relationships: FkRelationshipDefinition[],
originalRelationships: FkRelationshipDefinition[],
relationships: SqlTableRelation[],
originalRelationships: SqlTableRelation[],
): string {
const fullTableName = `[dbo].[${tableName}]`
const lines: string[] = [`/* ── ALTER TABLE: ${fullTableName} ── */`, '']
@ -412,7 +392,7 @@ function generateAlterTableSql(
const fkCascadeSql = (b: CascadeBehavior) =>
b === 'NoAction' ? 'NO ACTION' : b.replace(/([A-Z])/g, ' $1').trim().toUpperCase()
const addFkSql = (rel: FkRelationshipDefinition) => {
const addFkSql = (rel: SqlTableRelation) => {
const cname = rel.constraintName ?? `FK_${tableName}_${rel.fkColumnName}`
lines.push(`-- 🔗 FK Ekle: [${rel.fkColumnName}] → [${rel.referencedTable}]`)
lines.push(`ALTER TABLE ${fullTableName}`)
@ -424,9 +404,9 @@ function generateAlterTableSql(
lines.push('')
}
const origRelById = new Map<string, FkRelationshipDefinition>()
const origRelById = new Map<string, SqlTableRelation>()
originalRelationships.forEach((r) => origRelById.set(r.id, r))
const curRelById = new Map<string, FkRelationshipDefinition>()
const curRelById = new Map<string, SqlTableRelation>()
relationships.forEach((r) => curRelById.set(r.id, r))
// Removed FKs → DROP CONSTRAINT
@ -478,18 +458,6 @@ function generateAlterTableSql(
return lines.join('\n')
}
// Map TableDesigner data-type → SqlTableField type
function colTypeToEntityFieldType(
dt: SqlDataType,
): CreateUpdateSqlTableFieldDto['type'] {
if (dt === 'int' || dt === 'bigint') return 'number'
if (dt === 'bit') return 'boolean'
if (dt === 'datetime2' || dt === 'date') return 'date'
if (dt === 'uniqueidentifier') return 'guid'
if (dt === 'decimal' || dt === 'float' || dt === 'money') return 'decimal'
return 'string' // nvarchar, nvarchar(MAX), etc.
}
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const
type Step = 0 | 1 | 2 | 3
@ -508,11 +476,6 @@ const DEFAULT_SETTINGS: TableSettings = {
menuPrefix: '',
entityName: '',
tableName: '',
displayName: '',
description: '',
isActive: true,
isFullAudited: true,
isMultiTenant: false,
}
// ─── Component ────────────────────────────────────────────────────────────────
@ -536,12 +499,12 @@ const SqlTableDesignerDialog = ({
const [settings, setSettings] = useState<TableSettings>(DEFAULT_SETTINGS)
const [menuOptions, setMenuOptions] = useState<MenuOption[]>([])
const [menuLoading, setMenuLoading] = useState(false)
const [relationships, setRelationships] = useState<FkRelationshipDefinition[]>([])
const [originalRelationships, setOriginalRelationships] = useState<FkRelationshipDefinition[]>([])
const [relationships, setRelationships] = useState<SqlTableRelation[]>([])
const [originalRelationships, setOriginalRelationships] = useState<SqlTableRelation[]>([])
const [fksLoading, setFksLoading] = useState(false)
const [fkModalOpen, setFkModalOpen] = useState(false)
const [editingFkId, setEditingFkId] = useState<string | null>(null)
const [fkForm, setFkForm] = useState<Omit<FkRelationshipDefinition, 'id'>>(EMPTY_FK)
const [fkForm, setFkForm] = useState<Omit<SqlTableRelation, 'id'>>(EMPTY_FK)
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
const [targetColsLoading, setTargetColsLoading] = useState(false)
@ -583,42 +546,17 @@ const SqlTableDesignerDialog = ({
.catch(() => {})
.finally(() => setColsLoading(false))
// Load SqlTable metadata (menu, entityName, displayName, flags…)
developerKitService
.getSqlTables()
.then((res) => {
const match = (res.items ?? []).find(
(e) => e.tableName.toLowerCase() === initialTableData.tableName.toLowerCase(),
)
if (match) {
setSettings((s) => ({
...s,
menuValue: match.menu,
menuPrefix: match.menu,
entityName: match.name,
displayName: match.displayName,
description: match.description ?? '',
isActive: match.isActive,
isFullAudited: match.isFullAuditedEntity,
isMultiTenant: match.isMultiTenant,
}))
} else {
// Table not registered in developerKit — derive defaults from the table name.
// e.g. "Sas_T_SqlStoredProcedure" → menu: "Sas", entityName/displayName: "SqlStoredProcedure"
const parts = initialTableData.tableName.split('_')
const derivedMenu = parts[0] ?? ''
console.log('Derived menu from table name:', derivedMenu)
const derivedEntity = parts[parts.length - 1] ?? initialTableData.tableName
setSettings((s) => ({
...s,
menuValue: derivedMenu,
menuPrefix: derivedMenu,
entityName: derivedEntity,
displayName: derivedEntity,
}))
}
})
.catch(() => {})
// Derive settings from table name (e.g. "Sas_D_EntityName" → menu: "Sas", entity: "EntityName")
const parts = initialTableData.tableName.split('_')
const derivedMenu = parts[0] ?? ''
const derivedEntity = parts[parts.length - 1] ?? initialTableData.tableName
setSettings((s) => ({
...s,
menuValue: derivedMenu,
menuPrefix: derivedMenu,
entityName: derivedEntity,
displayName: derivedEntity,
}))
// Load existing FK constraints
const fkQuery = [
@ -649,7 +587,7 @@ const SqlTableDesignerDialog = ({
.executeQuery({ queryText: fkQuery, dataSourceCode: dataSource })
.then((res) => {
const rows: any[] = res.data?.data ?? []
const fkDefs: FkRelationshipDefinition[] = rows.map((r) => ({
const fkDefs: SqlTableRelation[] = rows.map((r) => ({
id: crypto.randomUUID(),
constraintName: r.constraintName,
relationshipType: 'OneToMany' as RelationshipType,
@ -687,6 +625,29 @@ const SqlTableDesignerDialog = ({
const addColumn = () => setColumns((prev) => [...prev, createEmptyColumn()])
const clearAllColumns = () => setColumns([createEmptyColumn()])
const addFullAuditedColumns = () => {
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
const toAdd = FULL_AUDIT_COLUMNS.filter(
(c) => !existingNames.has(c.columnName.toLowerCase()),
)
setColumns((prev) => {
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
})
}
const addMultiTenantColumns = () => {
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
if (!existingNames.has(TENANT_COLUMN.columnName.toLowerCase())) {
setColumns((prev) => {
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
return [...nonEmpty, { ...TENANT_COLUMN }, createEmptyColumn()]
})
}
}
const removeColumn = (id: string) => setColumns((prev) => prev.filter((c) => c.id !== id))
const moveColumn = (id: string, direction: 'up' | 'down') => {
@ -759,7 +720,7 @@ const SqlTableDesignerDialog = ({
setFkModalOpen(true)
}
const openEditFk = (rel: FkRelationshipDefinition) => {
const openEditFk = (rel: SqlTableRelation) => {
setEditingFkId(rel.id)
const { id: _id, ...rest } = rel
setFkForm(rest)
@ -801,10 +762,7 @@ const SqlTableDesignerDialog = ({
const baseOk =
!!settings.tableName.trim() && !!settings.entityName.trim() && !!settings.menuValue
if (!baseOk) return false
if (!settings.isFullAudited) {
return columns.some((c) => c.columnName.trim().toLowerCase() === 'id')
}
return true
return columns.some((c) => c.columnName.trim().toLowerCase() === 'id')
}
return true
}
@ -818,80 +776,6 @@ const SqlTableDesignerDialog = ({
// ── Deploy ─────────────────────────────────────────────────────────────────
const syncSqlTableMetadata = async (deployedTableName: string) => {
try {
const namedCols = columns.filter((c) => c.columnName.trim())
// Find existing SqlTable record by table name
const listResult = await developerKitService.getSqlTables()
const existing = (listResult.items ?? []).find(
(e) => e.tableName.toLowerCase() === deployedTableName.toLowerCase(),
)
if (existing) {
// Update: keep existing metadata, sync fields (match by name to preserve IDs)
const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => {
const existingField = existing.fields?.find(
(f) => f.name.toLowerCase() === col.columnName.trim().toLowerCase(),
)
const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined
return {
id: existingField?.id,
name: col.columnName.trim(),
type: colTypeToEntityFieldType(col.dataType),
isRequired: !col.isNullable,
maxLength: maxLen,
isUnique: false,
defaultValue: col.defaultValue || undefined,
description: col.description || undefined,
displayOrder: i,
}
})
const updateDto: CreateUpdateSqlTableDto = {
menu: existing.menu,
name: existing.name,
displayName: existing.displayName,
tableName: existing.tableName,
description: existing.description,
isActive: existing.isActive,
isFullAuditedEntity: existing.isFullAuditedEntity,
isMultiTenant: existing.isMultiTenant,
fields: fieldDtos,
}
await developerKitService.updateSqlTable(existing.id, updateDto)
} else {
// Create new SqlTable record
const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => {
const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined
return {
name: col.columnName.trim(),
type: colTypeToEntityFieldType(col.dataType),
isRequired: !col.isNullable,
maxLength: maxLen,
isUnique: false,
defaultValue: col.defaultValue || undefined,
description: col.description || undefined,
displayOrder: i,
}
})
const createDto: CreateUpdateSqlTableDto = {
menu: settings.menuValue,
name: settings.entityName || deployedTableName,
displayName: settings.displayName || deployedTableName,
tableName: deployedTableName,
description: settings.description || undefined,
isActive: settings.isActive,
isFullAuditedEntity: settings.isFullAudited,
isMultiTenant: settings.isMultiTenant,
fields: fieldDtos,
}
await developerKitService.createSqlTable(createDto)
}
} catch {
// Silent — SQL deploy already succeeded; metadata sync failure is non-critical
}
}
const handleDeploy = async () => {
if (!dataSource) {
toast.push(
@ -910,8 +794,6 @@ const SqlTableDesignerDialog = ({
})
if (result.data.success) {
const deployedTable = settings.tableName || initialTableData?.tableName || ''
// Sync entity metadata to SqlTable / SqlTableField tables
await syncSqlTableMetadata(deployedTable)
toast.push(
<Notification type="success" title="Basarili">
Tablo basariyla {isEditMode ? 'güncellendi' : 'oluşturuldu'}: [dbo].[{deployedTable}]
@ -996,31 +878,27 @@ const SqlTableDesignerDialog = ({
)}
{!colsLoading && (
<>
<div className="flex items-center justify-between py-3">
{isEditMode ? (
<div className="p-2 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded text-xs text-blue-700 dark:text-blue-300">
📝 Mevcut sütunlar yüklendi. Değişiklikler T-SQL Önizleme adımında <strong>ALTER TABLE</strong> olarak gösterilecek.
&nbsp;|
<span className="ml-1">
<span className="inline-block w-2.5 h-2.5 rounded bg-green-200 border border-green-400 mr-0.5"></span>Yeni
<span className="inline-block w-2.5 h-2.5 rounded bg-blue-200 border border-blue-400 mx-0.5 ml-2"></span>Ad Değişti (sp_rename)
<span className="inline-block w-2.5 h-2.5 rounded bg-yellow-200 border border-yellow-400 mx-0.5 ml-2"></span>Tip/Null Değişti
</span>
</div>
) : (
<div className="p-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded text-xs text-amber-700 dark:text-amber-300">
Sonraki adımda seceğiniz{' '}
<strong>Full Audited Entity</strong> / <strong>Multi-Tenant</strong> ayarlarina gore ek
sutunlar da dahil edilecektir.
</div>
)}
<Button size="xs" variant="solid" color="blue-600" icon={<FaPlus />} onClick={addColumn}>
Ekle
</Button>
<div className="flex items-center justify-between py-2">
<div className="flex items-center gap-2">
<Button size="xs" variant="solid" color="blue-600" onClick={addFullAuditedColumns}>
Full Audited Entity Sütunları Ekle
</Button>
<Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}>
MultiTenant Sütunları Ekle
</Button>
</div>
<div className="flex items-center gap-2">
<Button size="xs" variant="solid" color="red-600" icon={<FaTrash />} onClick={clearAllColumns}>
Tüm Sütunları Sil
</Button>
<Button size="xs" variant="solid" color="blue-600" icon={<FaPlus />} onClick={addColumn}>
Sütun Ekle
</Button>
</div>
</div>
{/* Header row */}
<div className="grid grid-cols-12 gap-2 px-2 py-1 bg-gray-50 dark:bg-gray-800 rounded text-xs font-semibold text-gray-600 dark:text-gray-300">
<div className="grid grid-cols-12 gap-1 px-1 py-1 bg-gray-50 dark:bg-gray-800 rounded text-xs font-semibold text-gray-600 dark:text-gray-300">
<div className="col-span-3">Sutun Adi *</div>
<div className="col-span-3">Veri Tipi *</div>
<div className="col-span-1 text-center">Max</div>
@ -1032,7 +910,7 @@ const SqlTableDesignerDialog = ({
{/* Editable column rows */}
{duplicateColumnNames.size > 0 && (
<div className="px-2 py-1.5 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-xs text-red-600 dark:text-red-400">
<div className="px-1 py-1.5 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-xs text-red-600 dark:text-red-400">
Aynı isimde sütun tanımlanamaz:{' '}
{[...duplicateColumnNames].map((n) => (
<code key={n} className="bg-red-100 dark:bg-red-900/40 px-1 rounded mr-1">
@ -1079,7 +957,7 @@ const SqlTableDesignerDialog = ({
return (
<div
key={col.id}
className={`grid grid-cols-12 gap-2 py-1 bg-white dark:bg-gray-800 rounded items-center ${rowBg}`}
className={`grid grid-cols-12 gap-2 bg-white dark:bg-gray-800 rounded items-center ${rowBg}`}
>
<div className="col-span-3">
<input
@ -1174,8 +1052,8 @@ const SqlTableDesignerDialog = ({
})}
</div>
{/* Id warning when Full Audited is off */}
{!isEditMode && !settings.isFullAudited &&
{/* Id warning */}
{!isEditMode &&
!columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
<div className="px-2 py-1.5 bg-orange-50 dark:bg-orange-900/20 border border-orange-300 dark:border-orange-700 rounded text-xs text-orange-700 dark:text-orange-300">
<strong>Full Audited Entity</strong> seçilmedi. Tablonun birincil anahtarı için{' '}
@ -1194,14 +1072,14 @@ const SqlTableDesignerDialog = ({
<div className="flex flex-col gap-2">
<div className="grid grid-cols-2 gap-2">
{/* Menu Name */}
<div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">
Menu Name <span className="text-red-500">*</span>
</label>
<select
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={settings.menuValue}
disabled={menuLoading}
disabled={menuLoading || isEditMode} // Menu cannot be changed in edit mode (as it determines the table name)
onChange={(e) => onMenuChange(e.target.value)}
>
<option value=""></option>
@ -1221,6 +1099,7 @@ const SqlTableDesignerDialog = ({
</label>
<input
type="text"
disabled={isEditMode} // Entity name (and thus table name) cannot be changed in edit mode
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={settings.entityName}
onChange={(e) => onEntityNameChange(e.target.value)}
@ -1238,101 +1117,12 @@ const SqlTableDesignerDialog = ({
value={isEditMode ? (initialTableData?.tableName ?? '') : settings.tableName}
placeholder="Menu ve Entity Name seçince otomatik oluşur"
/>
{isEditMode && (
<p className="text-xs text-blue-500 mt-0.5">Mevcut tablo ad değiştirilemez.</p>
)}
</div>
{/* Display Name */}
<div>
<label className="block text-sm font-medium mb-1">Display Name</label>
<input
type="text"
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={settings.displayName}
onChange={(e) => setSettings((s) => ({ ...s, displayName: e.target.value }))}
placeholder="e.g. Ürün, Kullanıcı, Sipariş"
/>
</div>
{/* Description */}
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
rows={2}
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white resize-none"
value={settings.description}
onChange={(e) => setSettings((s) => ({ ...s, description: e.target.value }))}
placeholder="Brief description of this entity"
/>
</div>
</div>
{/* Feature flags */}
<div className="border rounded-lg p-2 bg-gray-50 dark:bg-gray-800 space-y-2">
<h6 className="text-sm font-semibold mb-1">Entity Ozellikleri</h6>
{/* Active */}
<div className="flex items-start gap-3">
<Checkbox
checked={settings.isActive}
onChange={(checked) => setSettings((s) => ({ ...s, isActive: checked as boolean }))}
/>
<div>
<p className="text-sm font-medium">Active</p>
<p className="text-xs text-gray-500">Entity aktif mi olacak?</p>
</div>
</div>
{/* Full Audited */}
<div className="flex items-start gap-2">
<Checkbox
checked={settings.isFullAudited}
onChange={(checked) =>
setSettings((s) => ({ ...s, isFullAudited: checked as boolean }))
}
/>
<div>
<p className="text-sm font-medium">Full Audited Entity</p>
<p className="text-xs text-gray-500 mt-0.5">
{[
'Id',
'CreationTime',
'CreatorId',
'LastModificationTime',
'LastModifierId',
'IsDeleted',
'DeletionTime',
'DeleterId',
].map((c) => (
<code key={c} className="bg-gray-100 dark:bg-gray-700 px-1 rounded mr-0.5 text-xs">
{c}
</code>
))}
</p>
</div>
</div>
{/* Multi-Tenant */}
<div className="flex items-start gap-3">
<Checkbox
checked={settings.isMultiTenant}
onChange={(checked) =>
setSettings((s) => ({ ...s, isMultiTenant: checked as boolean }))
}
/>
<div>
<p className="text-sm font-medium">Multi-Tenant</p>
<p className="text-xs text-gray-500 mt-0.5">
<code className="bg-gray-100 dark:bg-gray-700 rounded text-xs">TenantId</code>{' '}
(uniqueidentifier NULL)
</p>
</div>
</div>
</div>
{/* Warning: no Id column when Full Audited is off */}
{!isEditMode && !settings.isFullAudited &&
{/* Warning: no Id column */}
{!isEditMode &&
!columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded text-xs text-red-700 dark:text-red-300">
<strong>Full Audited Entity</strong> seçili değil. Geri dönüp{' '}
@ -1433,7 +1223,7 @@ const SqlTableDesignerDialog = ({
</>}
{/* FK Add/Edit Modal */}
{fkModalOpen && (
{fkModalOpen && createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-lg flex flex-col">
{/* Modal Header */}
@ -1484,7 +1274,7 @@ const SqlTableDesignerDialog = ({
<select
value={fkForm.fkColumnName}
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
>
<option value=""> Seçiniz </option>
{columns
@ -1507,12 +1297,12 @@ const SqlTableDesignerDialog = ({
setFkForm((f) => ({ ...f, referencedTable: val, referencedColumn: '' }))
loadTargetColumns(val)
}}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
>
<option value=""> Seçiniz </option>
{dbTables.map((t) => (
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
[{t.schemaName}].[{t.tableName}]
{t.tableName}
</option>
))}
</select>
@ -1527,7 +1317,7 @@ const SqlTableDesignerDialog = ({
value={fkForm.referencedColumn}
onChange={(e) => setFkForm((f) => ({ ...f, referencedColumn: e.target.value }))}
disabled={targetColsLoading || targetTableColumns.length === 0}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60"
>
<option value=""> Önce hedef tablo seçin </option>
{targetTableColumns.map((col) => (
@ -1619,7 +1409,8 @@ const SqlTableDesignerDialog = ({
</button>
</div>
</div>
</div>
</div>,
document.body
)}
</div>
)
@ -1639,7 +1430,7 @@ const SqlTableDesignerDialog = ({
Kopyala
</button>
</div>
<pre className="bg-gray-900 text-green-300 rounded-lg p-4 text-xs overflow-auto max-h-96 font-mono leading-relaxed whitespace-pre">
<pre className="bg-gray-900 text-green-300 rounded-lg p-4 text-xs overflow-auto max-h-96 leading-relaxed whitespace-pre">
{generatedSql}
</pre>
</div>