From a0fa1b7e2b0ec41cd72c6a90706a7fa66d24349c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:52:01 +0300 Subject: [PATCH] =?UTF-8?q?Developer=20Kits=20d=C3=BCzenlemesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeveloperKit/CustomEntityDto.cs | 8 +- .../DeveloperKit/CrudMigrationAppService.cs | 115 ++++++++++++++++-- .../DeveloperKit/CustomEntityAppService.cs | 47 ++++++- .../Tenant/Administration/CustomEntity.cs | 4 +- .../PlatformDynamicEntityManager.cs | 22 ++-- .../DynamicData/MsDynamicDataRepository.cs | 4 +- ....cs => 20251105142841_Initial.Designer.cs} | 14 +-- ...9_Initial.cs => 20251105142841_Initial.cs} | 4 +- .../PlatformDbContextModelSnapshot.cs | 12 +- .../developerKit/CrudEndpointManager.tsx | 32 +++-- .../components/developerKit/EntityEditor.tsx | 87 +++++++++---- .../components/developerKit/EntityManager.tsx | 61 ++++++++-- ui/src/proxy/developerKit/models.ts | 8 +- 13 files changed, 314 insertions(+), 104 deletions(-) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20251105111749_Initial.Designer.cs => 20251105142841_Initial.Designer.cs} (99%) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20251105111749_Initial.cs => 20251105142841_Initial.cs} (99%) diff --git a/api/src/Kurs.Platform.Application.Contracts/DeveloperKit/CustomEntityDto.cs b/api/src/Kurs.Platform.Application.Contracts/DeveloperKit/CustomEntityDto.cs index 586a08ac..021f3a7c 100644 --- a/api/src/Kurs.Platform.Application.Contracts/DeveloperKit/CustomEntityDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/DeveloperKit/CustomEntityDto.cs @@ -11,8 +11,8 @@ public class CustomEntityDto : FullAuditedEntityDto public string TableName { get; set; } = string.Empty; public string? Description { get; set; } public bool IsActive { get; set; } = true; - public bool HasAuditFields { get; set; } = true; - public bool HasSoftDelete { get; set; } = true; + public bool IsFullAuditedEntity { get; set; } = true; + public bool IsMultiTenant { get; set; } = false; public string MigrationStatus { get; set; } = "pending"; public Guid? MigrationId { get; set; } public string EndpointStatus { get; set; } = "pending"; @@ -27,8 +27,8 @@ public class CreateUpdateCustomEntityDto public string TableName { get; set; } = string.Empty; public string? Description { get; set; } public bool IsActive { get; set; } = true; - public bool HasAuditFields { get; set; } = true; - public bool HasSoftDelete { get; set; } = true; + public bool IsFullAuditedEntity { get; set; } = true; + public bool IsMultiTenant { get; set; } = false; public List Fields { get; set; } = []; } diff --git a/api/src/Kurs.Platform.Application/DeveloperKit/CrudMigrationAppService.cs b/api/src/Kurs.Platform.Application/DeveloperKit/CrudMigrationAppService.cs index bb483d6a..097e957a 100644 --- a/api/src/Kurs.Platform.Application/DeveloperKit/CrudMigrationAppService.cs +++ b/api/src/Kurs.Platform.Application/DeveloperKit/CrudMigrationAppService.cs @@ -96,9 +96,20 @@ public class CrudMigrationAppService : CrudAppService< if (entity == null) throw new EntityNotFoundException($"CustomEntity with id {entityId} not found"); - var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}_Create{entity.Name}Table.sql"; + // Check if there are any previously applied migrations for this entity + var migrationQueryable = await _migrationRepository.GetQueryableAsync(); + var lastAppliedMigration = await migrationQueryable + .Where(m => m.EntityId == entityId && m.Status == "applied") + .OrderByDescending(m => m.CreationTime) + .FirstOrDefaultAsync(); - var sqlScript = GenerateSqlScript(entity); + // Use "Update" if migrations were applied before, otherwise "Create" + var operationType = lastAppliedMigration != null ? "Update" : "Create"; + var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}_{operationType}{entity.Name}Table.sql"; + + var sqlScript = lastAppliedMigration != null + ? GenerateAlterTableScript(entity, lastAppliedMigration) + : GenerateCreateTableScript(entity); var migration = new CrudMigration { @@ -118,7 +129,7 @@ public class CrudMigrationAppService : CrudAppService< return ObjectMapper.Map(migration); } - private string GenerateSqlScript(CustomEntity entity) + private string GenerateCreateTableScript(CustomEntity entity) { if (entity.Fields == null || !entity.Fields.Any()) throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined."); @@ -156,19 +167,22 @@ public class CrudMigrationAppService : CrudAppService< sb.AppendLine(","); } - if (entity.HasAuditFields) + // Add TenantId if entity is multi-tenant + if (entity.IsMultiTenant) + { + sb.AppendLine(" [TenantId] UNIQUEIDENTIFIER NULL,"); + } + + // Add Full Audited Entity fields (includes both audit and soft delete) + if (entity.IsFullAuditedEntity) { sb.AppendLine(" [CreationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,"); sb.AppendLine(" [CreatorId] UNIQUEIDENTIFIER NULL,"); - sb.AppendLine(" [LastModificationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,"); + sb.AppendLine(" [LastModificationTime] DATETIME2 NULL,"); sb.AppendLine(" [LastModifierId] UNIQUEIDENTIFIER NULL,"); - } - - if (entity.HasSoftDelete) - { sb.AppendLine(" [IsDeleted] BIT DEFAULT 0 NOT NULL,"); sb.AppendLine(" [DeleterId] UNIQUEIDENTIFIER NULL,"); - sb.AppendLine(" [DeletionTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,"); + sb.AppendLine(" [DeletionTime] DATETIME2 NULL,"); } // Remove last comma @@ -178,6 +192,87 @@ public class CrudMigrationAppService : CrudAppService< return script; } + private string GenerateAlterTableScript(CustomEntity entity, CrudMigration lastAppliedMigration) + { + if (entity.Fields == null || !entity.Fields.Any()) + throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined."); + + // Parse the last applied migration's SQL to understand what fields existed + var existingFieldNames = ExtractFieldNamesFromCreateScript(lastAppliedMigration.SqlScript); + + var sb = new StringBuilder(); + + sb.AppendLine($@"/* + # Update {entity.DisplayName} table + + 1. Alter Table + - [{entity.TableName}] + - Adding new fields or modifying existing ones"); + + sb.AppendLine("*/\n"); + + // Find new fields that need to be added + var newFields = entity.Fields.Where(f => !existingFieldNames.Contains(f.Name)).ToList(); + + if (newFields.Any()) + { + foreach (var field in newFields) + { + var sqlType = GetSqlType(field.Type, field.MaxLength); + sb.Append($"IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[{entity.TableName}]') AND name = '{field.Name}')\n"); + sb.Append($"BEGIN\n"); + sb.Append($" ALTER TABLE [{entity.TableName}] ADD [{field.Name}] {sqlType}"); + + if (field.IsRequired) + sb.Append(" NOT NULL"); + else + sb.Append(" NULL"); + + if (!string.IsNullOrWhiteSpace(field.DefaultValue)) + sb.Append($" DEFAULT {FormatDefaultValue(field.Type, field.DefaultValue)}"); + + sb.AppendLine(";"); + sb.AppendLine($"END\n"); + } + } + else + { + sb.AppendLine("-- No new fields to add"); + } + + return sb.ToString(); + } + + private HashSet ExtractFieldNamesFromCreateScript(string sqlScript) + { + var fieldNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Simple parsing - extract field names between [ and ] + var lines = sqlScript.Split('\n'); + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("[") && trimmedLine.Contains("]")) + { + var fieldName = trimmedLine.Substring(1, trimmedLine.IndexOf("]") - 1); + if (fieldName != "Id" && + fieldName != "TenantId" && + fieldName != "CreationTime" && + fieldName != "CreatorId" && + fieldName != "LastModificationTime" && + fieldName != "LastModifierId" && + fieldName != "IsDeleted" && + fieldName != "DeleterId" && + fieldName != "DeletionTime") + { + fieldNames.Add(fieldName); + } + } + } + + return fieldNames; + } + private string GetSqlType(string fieldType, int? maxLength) { return fieldType.ToLower() switch diff --git a/api/src/Kurs.Platform.Application/DeveloperKit/CustomEntityAppService.cs b/api/src/Kurs.Platform.Application/DeveloperKit/CustomEntityAppService.cs index d4232b90..5a43df4b 100644 --- a/api/src/Kurs.Platform.Application/DeveloperKit/CustomEntityAppService.cs +++ b/api/src/Kurs.Platform.Application/DeveloperKit/CustomEntityAppService.cs @@ -105,13 +105,26 @@ public class CustomEntityAppService : CrudAppService< if (entity == null) throw new EntityNotFoundException(typeof(CustomEntity), id); + // If entity structure has changed and migration was applied, reset migration status + bool structureChanged = entity.Name != input.Name || + entity.TableName != input.TableName || + entity.IsFullAuditedEntity != input.IsFullAuditedEntity || + entity.IsMultiTenant != input.IsMultiTenant || + FieldsHaveChanged(entity.Fields, input.Fields); + + if (structureChanged && entity.MigrationStatus == "applied") + { + entity.MigrationStatus = "pending"; + entity.MigrationId = null; + } + entity.Name = input.Name; entity.DisplayName = input.DisplayName; entity.TableName = input.TableName; entity.Description = input.Description; entity.IsActive = input.IsActive; - entity.HasAuditFields = input.HasAuditFields; - entity.HasSoftDelete = input.HasSoftDelete; + entity.IsFullAuditedEntity = input.IsFullAuditedEntity; + entity.IsMultiTenant = input.IsMultiTenant; var updatedFields = new List(); @@ -183,8 +196,8 @@ public class CustomEntityAppService : CrudAppService< TableName = input.TableName, Description = input.Description, IsActive = input.IsActive, - HasAuditFields = input.HasAuditFields, - HasSoftDelete = input.HasSoftDelete, + IsFullAuditedEntity = input.IsFullAuditedEntity, + IsMultiTenant = input.IsMultiTenant, MigrationStatus = "pending" }; @@ -212,6 +225,32 @@ public class CustomEntityAppService : CrudAppService< return ObjectMapper.Map(entity); } + private bool FieldsHaveChanged(ICollection existingFields, List 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; + } + public override async Task DeleteAsync(Guid id) { // İlgili entity'nin var olup olmadığını kontrol et diff --git a/api/src/Kurs.Platform.Domain/Entities/Tenant/Administration/CustomEntity.cs b/api/src/Kurs.Platform.Domain/Entities/Tenant/Administration/CustomEntity.cs index 2f2fd40a..e9f9821b 100644 --- a/api/src/Kurs.Platform.Domain/Entities/Tenant/Administration/CustomEntity.cs +++ b/api/src/Kurs.Platform.Domain/Entities/Tenant/Administration/CustomEntity.cs @@ -14,8 +14,8 @@ public class CustomEntity : FullAuditedEntity, IMultiTenant public string TableName { get; set; } = string.Empty; public string? Description { get; set; } public bool IsActive { get; set; } = true; - public bool HasAuditFields { get; set; } = true; - public bool HasSoftDelete { get; set; } = true; + public bool IsFullAuditedEntity { get; set; } = true; + public bool IsMultiTenant { get; set; } = false; public string MigrationStatus { get; set; } = "pending"; public Guid? MigrationId { get; set; } public string EndpointStatus { get; set; } = "pending"; // "pending" | "applied" | "failed" diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs b/api/src/Kurs.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs index 0e1c84d7..861f6dd9 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/DeveloperKit/PlatformDynamicEntityManager.cs @@ -54,10 +54,16 @@ public class DynamicEntityManager : IDynamicEntityManager } } - if (entity.HasAuditFields) + if (entity.IsMultiTenant && data.TryGetProperty("TenantId", out var tenantIdValue)) { - columns.AddRange(new[] { "[CreationTime]", "[LastModificationTime]" }); - values.AddRange(new[] { "SYSUTCDATETIME()", "SYSUTCDATETIME()" }); + 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)})"; @@ -86,7 +92,7 @@ public class DynamicEntityManager : IDynamicEntityManager } } - if (entity.HasAuditFields) + if (entity.IsFullAuditedEntity) setParts.Add("[LastModificationTime] = SYSUTCDATETIME()"); var updateQuery = $"UPDATE [{tableName}] SET {string.Join(", ", setParts)} WHERE Id = '{id}'"; @@ -107,12 +113,10 @@ public class DynamicEntityManager : IDynamicEntityManager return false; string deleteQuery; - if (entity.HasSoftDelete) + if (entity.IsFullAuditedEntity) { - var parts = new List { "[IsDeleted] = 1" }; - if (entity.HasAuditFields) - parts.Add("[DeletionTime] = SYSUTCDATETIME()"); - deleteQuery = $"UPDATE [{tableName}] SET {string.Join(", ", parts)} WHERE Id = '{id}'"; + // Full audited entities always have soft delete + deleteQuery = $"UPDATE [{tableName}] SET [IsDeleted] = 1, [DeletionTime] = SYSUTCDATETIME() WHERE Id = '{id}'"; } else { diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs index 43460710..58c039d3 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs @@ -39,7 +39,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency { transaction = await con.BeginTransactionAsync(); unitOfWorkManager.Current.AddTransactionApi(key, new DapperTransactionApi(transaction, cancellationTokenProvider)); - transactions.Add(key, transaction); + transactions[key] = transaction; unitOfWorkManager.Current.OnCompleted(() => { transaction = null; @@ -56,7 +56,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency if (connection == null) { connection = new SqlConnection(cs); - connections.Add(key, connection); + connections[key] = connection; // Use indexer instead of Add to avoid duplicate key exception } if (connection.State != ConnectionState.Open) { diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105111749_Initial.Designer.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105142841_Initial.Designer.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105111749_Initial.Designer.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105142841_Initial.Designer.cs index 4d71978f..e94445fa 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105111749_Initial.Designer.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105142841_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Kurs.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20251105111749_Initial")] + [Migration("20251105142841_Initial")] partial class Initial { /// @@ -2928,12 +2928,6 @@ namespace Kurs.Platform.Migrations .HasMaxLength(50) .HasColumnType("nvarchar(50)"); - b.Property("HasAuditFields") - .HasColumnType("bit"); - - b.Property("HasSoftDelete") - .HasColumnType("bit"); - b.Property("IsActive") .HasColumnType("bit"); @@ -2943,6 +2937,12 @@ namespace Kurs.Platform.Migrations .HasDefaultValue(false) .HasColumnName("IsDeleted"); + b.Property("IsFullAuditedEntity") + .HasColumnType("bit"); + + b.Property("IsMultiTenant") + .HasColumnType("bit"); + b.Property("LastModificationTime") .HasColumnType("datetime2") .HasColumnName("LastModificationTime"); diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105111749_Initial.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105142841_Initial.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105111749_Initial.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105142841_Initial.cs index be325691..405f7fe7 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105111749_Initial.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251105142841_Initial.cs @@ -2137,8 +2137,8 @@ namespace Kurs.Platform.Migrations TableName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), IsActive = table.Column(type: "bit", nullable: false), - HasAuditFields = table.Column(type: "bit", nullable: false), - HasSoftDelete = table.Column(type: "bit", nullable: false), + IsFullAuditedEntity = table.Column(type: "bit", nullable: false), + IsMultiTenant = table.Column(type: "bit", nullable: false), MigrationStatus = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), MigrationId = table.Column(type: "uniqueidentifier", nullable: true), EndpointStatus = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 93409530..dabbdfca 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -2925,12 +2925,6 @@ namespace Kurs.Platform.Migrations .HasMaxLength(50) .HasColumnType("nvarchar(50)"); - b.Property("HasAuditFields") - .HasColumnType("bit"); - - b.Property("HasSoftDelete") - .HasColumnType("bit"); - b.Property("IsActive") .HasColumnType("bit"); @@ -2940,6 +2934,12 @@ namespace Kurs.Platform.Migrations .HasDefaultValue(false) .HasColumnName("IsDeleted"); + b.Property("IsFullAuditedEntity") + .HasColumnType("bit"); + + b.Property("IsMultiTenant") + .HasColumnType("bit"); + b.Property("LastModificationTime") .HasColumnType("datetime2") .HasColumnName("LastModificationTime"); diff --git a/ui/src/components/developerKit/CrudEndpointManager.tsx b/ui/src/components/developerKit/CrudEndpointManager.tsx index bec60041..dd5f9857 100644 --- a/ui/src/components/developerKit/CrudEndpointManager.tsx +++ b/ui/src/components/developerKit/CrudEndpointManager.tsx @@ -485,14 +485,14 @@ const CrudEndpointManager: React.FC = () => { {/* Endpoints List */} {filteredEndpoints.length > 0 ? ( -
+
{filteredEndpoints.map((endpoint) => (
{ const newSelectedEndpoint = selectedEndpoint === endpoint.id ? null : endpoint.id setSelectedEndpoint(newSelectedEndpoint) @@ -505,7 +505,7 @@ const CrudEndpointManager: React.FC = () => { {/* Sol taraf */}
@@ -513,37 +513,33 @@ const CrudEndpointManager: React.FC = () => {

{endpoint.name}

- - {endpoint.path} -
{/* Sağ taraf */}
- {endpoint.type === 'generated' && ( - - {translate('::App.DeveloperKit.Endpoint.AutoGenerated')} - - )} -
- + {translate('::App.DeveloperKit.Endpoint.Active')}
- - {endpoint.description && ( -

{endpoint.description}

- )} + +
+ + {endpoint.path} + + {endpoint.description && ( +

{endpoint.description}

+ )} +
{/* Expanded Details */} {selectedEndpoint === endpoint.id && ( -
+
{/* Request Details */}
diff --git a/ui/src/components/developerKit/EntityEditor.tsx b/ui/src/components/developerKit/EntityEditor.tsx index c5a8ca71..96b1de3a 100644 --- a/ui/src/components/developerKit/EntityEditor.tsx +++ b/ui/src/components/developerKit/EntityEditor.tsx @@ -40,8 +40,8 @@ const validationSchema = Yup.object({ ) .min(1, 'At least one field is required'), isActive: Yup.boolean(), - hasAuditFields: Yup.boolean(), - hasSoftDelete: Yup.boolean(), + isFullAuditedEntity: Yup.boolean(), + isMultiTenant: Yup.boolean(), }) const EntityEditor: React.FC = () => { @@ -72,15 +72,10 @@ const EntityEditor: React.FC = () => { }, ] as CustomEntityField[], isActive: true, - hasAuditFields: true, - hasSoftDelete: true, + isFullAuditedEntity: true, + isMultiTenant: true, }) - // Check if migration is applied to disable certain fields - const isMigrationApplied = Boolean( - isEditing && id && getEntity(id)?.migrationStatus === 'applied', - ) - useEffect(() => { if (isEditing && id) { const entity = getEntity(id) @@ -98,8 +93,8 @@ const EntityEditor: React.FC = () => { description: entity.description || '', fields: sortedFields, isActive: entity.isActive, - hasAuditFields: entity.hasAuditFields, - hasSoftDelete: entity.hasSoftDelete, + isFullAuditedEntity: entity.isFullAuditedEntity, + isMultiTenant: entity.isMultiTenant, }) } } @@ -132,8 +127,8 @@ const EntityEditor: React.FC = () => { description: values.description.trim(), fields: sanitizedFields, isActive: values.isActive, - hasAuditFields: values.hasAuditFields, - hasSoftDelete: values.hasSoftDelete, + isFullAuditedEntity: values.isFullAuditedEntity, + isMultiTenant: values.isMultiTenant, } if (isEditing && id) { @@ -208,7 +203,17 @@ const EntityEditor: React.FC = () => { type="button" onClick={submitForm} disabled={isSubmitting || !isValid} - className="flex items-center gap-1 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold px-2 py-1.5 rounded shadow transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed text-sm" + className={` + flex items-center gap-1 + px-2 py-1.5 rounded text-sm transition-all duration-200 + text-white + bg-gradient-to-r from-green-600 to-green-700 + hover:from-green-700 hover:to-green-800 + disabled:from-gray-400 disabled:to-gray-500 + disabled:text-gray-200 + disabled:cursor-not-allowed + disabled:hover:from-gray-400 disabled:hover:to-gray-500 + `} > {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} @@ -219,6 +224,25 @@ const EntityEditor: React.FC = () => {
+ {/* Migration Status Info Banner */} + {isEditing && id && getEntity(id)?.migrationStatus === 'applied' && ( +
+
+
+ +
+
+

+ Migration Applied - Changes Will Require New Migration +

+

+ This entity has been migrated to the database. Any structural changes you make will reset the migration status to "pending", and you'll need to generate and apply a new migration to update the database schema. +

+
+
+
+ )} + {/* Basic Entity Information */}
@@ -248,7 +272,6 @@ const EntityEditor: React.FC = () => { setFieldValue('displayName', values.name) } }} - disabled={isMigrationApplied} placeholder="e.g., Product, User, Order" className="px-2 py-1.5 bg-slate-50 focus:bg-white transition-all duration-200 text-sm h-7" /> @@ -276,7 +299,6 @@ const EntityEditor: React.FC = () => { @@ -290,6 +312,8 @@ const EntityEditor: React.FC = () => { name="description" component={Input} placeholder="Brief description of this entity" + textArea={true} + rows={3} /> @@ -297,12 +321,18 @@ const EntityEditor: React.FC = () => { - - + + +

+ Includes CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeleterId, DeletionTime +

- - + + +

+ Adds TenantId column for multi-tenancy support +

@@ -334,19 +364,28 @@ const EntityEditor: React.FC = () => { type: 'string', isRequired: false, description: '', - // Assign next sequential displayOrder displayOrder: (values.fields?.length ?? 0) + 1, }) } - className="flex items-center gap-1 bg-gradient-to-r from-blue-600 to-blue-700 text-white px-2 py-1.5 rounded hover:from-blue-700 hover:to-blue-800 transition-all duration-200 text-sm" - > + className={` + flex items-center gap-1 + px-2 py-1.5 rounded text-sm transition-all duration-200 + text-white + bg-gradient-to-r from-blue-600 to-blue-700 + hover:from-blue-700 hover:to-blue-800 + disabled:from-gray-400 disabled:to-gray-500 + disabled:text-gray-200 + disabled:cursor-not-allowed + disabled:hover:from-gray-400 disabled:hover:to-gray-500 + `} + > {translate('::App.DeveloperKit.EntityEditor.AddField')}
-
Order *
+
Order *
{translate('::App.DeveloperKit.EntityEditor.FieldName')} * diff --git a/ui/src/components/developerKit/EntityManager.tsx b/ui/src/components/developerKit/EntityManager.tsx index 0b54891a..55259116 100644 --- a/ui/src/components/developerKit/EntityManager.tsx +++ b/ui/src/components/developerKit/EntityManager.tsx @@ -14,14 +14,16 @@ import { FaCheckCircle, FaTable, FaBolt, + FaSync, } from 'react-icons/fa' import { ROUTES_ENUM } from '@/routes/route.constant' import { useLocalization } from '@/utils/hooks/useLocalization' const EntityManager: React.FC = () => { - const { entities, deleteEntity, toggleEntityActiveStatus, refreshEntities } = useEntities() + const { entities, deleteEntity, toggleEntityActiveStatus, refreshEntities, generateMigration } = useEntities() const [searchTerm, setSearchTerm] = useState('') const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all') + const [generatingMigration, setGeneratingMigration] = useState(null) const { translate } = useLocalization() // Sayfa odaklandığında varlıkları yenile @@ -77,6 +79,19 @@ const EntityManager: React.FC = () => { } } + const handleGenerateMigration = async (entityId: string) => { + try { + setGeneratingMigration(entityId) + await generateMigration(entityId) + alert('Migration generated successfully!') + } catch (err) { + console.error('Failed to generate migration:', err) + alert('Failed to generate migration. Please try again.') + } finally { + setGeneratingMigration(null) + } + } + const stats = { total: entities.length, active: entities.filter((e) => e.isActive).length, @@ -287,17 +302,39 @@ const EntityManager: React.FC = () => { {translate('::App.DeveloperKit.Entity.MigrationStatus')} - - {entity.migrationStatus} - +
+ + {entity.migrationStatus} + + {entity.migrationStatus === 'pending' && ( + + )} +
diff --git a/ui/src/proxy/developerKit/models.ts b/ui/src/proxy/developerKit/models.ts index dd2f52af..840a5db4 100644 --- a/ui/src/proxy/developerKit/models.ts +++ b/ui/src/proxy/developerKit/models.ts @@ -5,8 +5,8 @@ export interface CustomEntity { tableName: string; description?: string; isActive: boolean; - hasAuditFields: boolean; - hasSoftDelete: boolean; + isFullAuditedEntity: boolean; + isMultiTenant: boolean; migrationStatus: "pending" | "applied" | "failed"; endpointStatus: "pending" | "applied" | "failed"; migrationId?: string; @@ -48,8 +48,8 @@ export interface CreateUpdateCustomEntityDto { tableName: string; description?: string; isActive: boolean; - hasAuditFields: boolean; - hasSoftDelete: boolean; + isFullAuditedEntity: boolean; + isMultiTenant: boolean; fields: CreateUpdateCustomEntityFieldDto[]; }