diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1fcf92be..e32e2120 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,46 +1,46 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "before-build", - "type": "shell", - "command": "dotnet build src/Client/CrystaLearn.Client.Core/CrystaLearn.Client.Core.csproj -t:BeforeBuildTasks", - "runOptions": { - "runOn": "folderOpen" - } - }, - { - "label": "build", - "type": "shell", - "command": "dotnet build src/Server/CrystaLearn.Server.Web/CrystaLearn.Server.Web.csproj" - }, - { - "label": "generate-resx-files", - "type": "shell", - "command": "dotnet build src/Shared/CrystaLearn.Shared.csproj -t:PrepareResources" - }, - { - "label": "run", - "type": "shell", - "command": "dotnet", - "args": [ - "run" - ], - "options": { - "cwd": "${workspaceFolder}/src/Server/CrystaLearn.Server.Web" - }, - "isBackground": true - }, - { - "label": "run-tests", - "type": "shell", - "command": "dotnet", - "args": [ - "test" - ], - "options": { - "cwd": "${workspaceFolder}/src/Tests" - } - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "before-build", + "type": "shell", + "command": "dotnet build src/Client/CrystaLearn.Client.Core/CrystaLearn.Client.Core.csproj -t:BeforeBuildTasks", + "runOptions": { + "runOn": "folderOpen" + } + }, + { + "label": "build", + "type": "shell", + "command": "dotnet build src/Server/CrystaLearn.Server.Web/CrystaLearn.Server.Web.csproj" + }, + { + "label": "generate-resx-files", + "type": "shell", + "command": "dotnet build src/Shared/CrystaLearn.Shared.csproj -t:PrepareResources" + }, + { + "label": "run", + "type": "shell", + "command": "dotnet", + "args": [ + "run" + ], + "options": { + "cwd": "${workspaceFolder}/src/Server/CrystaLearn.Server.Web" + }, + "isBackground": true + }, + { + "label": "run-tests", + "type": "shell", + "command": "dotnet", + "args": [ + "test" + ], + "options": { + "cwd": "${workspaceFolder}/src/Tests" + } + } + ] } \ No newline at end of file diff --git a/docs/architecture/sync-overview.md b/docs/architecture/sync-overview.md index e933ff93..5617477d 100644 --- a/docs/architecture/sync-overview.md +++ b/docs/architecture/sync-overview.md @@ -23,7 +23,7 @@ graph TB subgraph "CrystaLearn Sync Layer" ABS[AzureBoardSyncService] - GHS[GitHubService] + GHS[GitHubSyncService] LIS[LinkedInSyncService*] TWS[TwitterSyncService*] end @@ -146,7 +146,7 @@ Each synced entity follows a consistent three-layer service architecture: - Orchestrates synchronization between external source and local database - Handles change detection via hash comparison - Manages incremental sync using timestamps and offsets - - Example: `AzureBoardSyncService` + - Example: `AzureBoardSyncService`, `GithubSyncService` 3. **Provider Service Layer** (`[Provider]Service`) - Low-level communication with external APIs diff --git a/src/Core/CrystaLearn.Core.Test/Sync/GithubSyncServiceTests.cs b/src/Core/CrystaLearn.Core.Test/Sync/GithubSyncServiceTests.cs new file mode 100644 index 00000000..62c8434f --- /dev/null +++ b/src/Core/CrystaLearn.Core.Test/Sync/GithubSyncServiceTests.cs @@ -0,0 +1,86 @@ +using CrystaLearn.Core.Extensions; +using CrystaLearn.Core.Models.Crysta; +using CrystaLearn.Core.Services; +using CrystaLearn.Core.Services.Contracts; +using CrystaLearn.Core.Services.Sync; +using CrystaLearn.Core.Tests.Infra; + +namespace CrystaLearn.Core.Tests.Sync; + +public class GitHubSyncServiceTests : TestBase +{ + [Fact] + public async Task SyncGithubDocuments_WithValidModule_MustWork() + { + // Arrange + var services = CreateServiceProvider(configServices: (sc) => + { + sc.AddTransient(); + sc.AddTransient(); + sc.AddTransient(); + sc.AddSingleton(); + }); + + using var scope = services.CreateScope(); + var service = scope.ServiceProvider.GetRequiredService(); + var programRepo = scope.ServiceProvider.GetRequiredService(); + + // Get a test program + var programs = await programRepo.GetCrystaProgramsAsync(CancellationToken.None); + var testProgram = programs.FirstOrDefault(); + + if (testProgram == null) + { + // Skip test if no programs available + return; + } + + var module = new CrystaProgramSyncModule + { + Id = Guid.NewGuid(), + CrystaProgramId = testProgram.Id, + CrystaProgram = testProgram, + ModuleType = SyncModuleType.GitHubDocument, + SyncInfo = new SyncInfo + { + LastSyncDateTime = DateTimeOffset.Now.AddYears(-1), + LastSyncOffset = "0" + } + }; + + // Act + var result = await service.SyncAsync(module); + + // Assert + Assert.NotNull(result); + Assert.True(result.AddCount >= 0); + Assert.True(result.UpdateCount >= 0); + Assert.True(result.SameCount >= 0); + Assert.True(result.DeleteCount >= 0); + } + + [Fact] + public async Task SyncGithubDocuments_WithInvalidModuleType_MustThrowException() + { + // Arrange + var services = CreateServiceProvider(configServices: (sc) => + { + sc.AddTransient(); + sc.AddSingleton(); + }); + + using var scope = services.CreateScope(); + var service = scope.ServiceProvider.GetRequiredService(); + + var module = new CrystaProgramSyncModule + { + Id = Guid.NewGuid(), + ModuleType = SyncModuleType.AzureBoard, // Wrong type + SyncInfo = new SyncInfo() + }; + + // Act & Assert + await Assert.ThrowsAsync(async () => + await service.SyncAsync(module)); + } +} diff --git a/src/Core/CrystaLearn.Core/Data/AppDbContext.cs b/src/Core/CrystaLearn.Core/Data/AppDbContext.cs index e2a0860c..2b7b73f0 100644 --- a/src/Core/CrystaLearn.Core/Data/AppDbContext.cs +++ b/src/Core/CrystaLearn.Core/Data/AppDbContext.cs @@ -37,6 +37,8 @@ public partial class AppDbContext(DbContextOptions options) public DbSet CrystaProgramSyncModules { get; set; } = default!; + public DbSet CrystaDocument { get; set; } = default!; + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/src/Core/CrystaLearn.Core/Data/Configurations/Crysta/CrystaDocumentConfiguration.cs b/src/Core/CrystaLearn.Core/Data/Configurations/Crysta/CrystaDocumentConfiguration.cs new file mode 100644 index 00000000..16e08d91 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Data/Configurations/Crysta/CrystaDocumentConfiguration.cs @@ -0,0 +1,22 @@ +using CrystaLearn.Core.Models.Crysta; + +namespace CrystaLearn.Core.Data.Configurations.Crysta; + +public class CrystaDocumentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + //builder.HasIndex(d => d.Code).IsUnique(); + builder.HasIndex(d => d.IsActive); + builder.HasIndex(d => d.CrystaProgramId); + + builder.HasOne(d => d.CrystaProgram) + .WithMany() + .HasForeignKey(d => d.CrystaProgramId); + + builder.OwnsOne(d => d.SyncInfo, sync => + { + sync.HasIndex(s => s.SyncId).IsUnique(); + }); + } +} diff --git a/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs b/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs index acee4304..6c1a3d28 100644 --- a/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Core/CrystaLearn.Core/Extensions/ApplicationBuilderExtensions.cs @@ -30,13 +30,16 @@ void AddDbContext(DbContextOptionsBuilder options) builder.AddGitHubClient(); - services.AddSingleton(); - services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); services.AddTransient(); } diff --git a/src/Core/CrystaLearn.Core/Mappers/DocumentMapper.cs b/src/Core/CrystaLearn.Core/Mappers/DocumentMapper.cs index f081d109..5d0a0e5f 100644 --- a/src/Core/CrystaLearn.Core/Mappers/DocumentMapper.cs +++ b/src/Core/CrystaLearn.Core/Mappers/DocumentMapper.cs @@ -7,5 +7,5 @@ namespace CrystaLearn.Core.Mappers; [Mapper] public static partial class DocumentMapper { - public static partial DocumentDto Map(this Document source); + public static partial DocumentDto Map(this CrystaDocument source); } diff --git a/src/Core/CrystaLearn.Core/Migrations/20251220121952_AddCrystaDocument.Designer.cs b/src/Core/CrystaLearn.Core/Migrations/20251220121952_AddCrystaDocument.Designer.cs new file mode 100644 index 00000000..7453c1cc --- /dev/null +++ b/src/Core/CrystaLearn.Core/Migrations/20251220121952_AddCrystaDocument.Designer.cs @@ -0,0 +1,2525 @@ +// +using System; +using CrystaLearn.Core.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CrystaLearn.Core.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251220121952_AddCrystaDocument")] + partial class AddCrystaDocument + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("CrystaLearn") + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CrystaLearn.Core.Models.Attachments.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id", "Kind"); + + b.ToTable("Attachments", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Chatbot.SystemPrompt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("text"); + + b.Property("PromptKind") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PromptKind") + .IsUnique(); + + b.ToTable("SystemPrompts", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("FileExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FileName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("FileNameWithoutExtension") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Folder") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SourceContentUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SourceHtmlUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("IsActive"); + + b.ToTable("CrystaDocument", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgram", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BadgeSystemUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.ToTable("CrystaPrograms", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgramSyncModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("ModuleType") + .HasColumnType("integer"); + + b.Property("SyncConfig") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("ModuleType"); + + b.ToTable("CrystaProgramSyncModules", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AreaId") + .HasColumnType("uuid"); + + b.Property("AreaPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AssignedToId") + .HasColumnType("uuid"); + + b.Property("AssignedToText") + .HasColumnType("text"); + + b.Property("AttachmentsCount") + .HasColumnType("integer"); + + b.Property("BoardColumn") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BoardColumnDone") + .HasColumnType("boolean"); + + b.Property("ChangedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChangedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CommentCount") + .HasColumnType("integer"); + + b.Property("CompletedById") + .HasColumnType("uuid"); + + b.Property("CompletedByText") + .HasColumnType("text"); + + b.Property("CompletedWork") + .HasColumnType("double precision"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedById1") + .HasColumnType("uuid"); + + b.Property("CreatedByText") + .HasColumnType("text"); + + b.Property("CreatedFromRevisionId") + .HasColumnType("integer"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CustomFields") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DescriptionHtml") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IterationId") + .HasColumnType("uuid"); + + b.Property("IterationPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Links") + .HasColumnType("text"); + + b.Property("OriginalEstimate") + .HasColumnType("double precision"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProjectName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProviderParentId") + .HasColumnType("text"); + + b.Property("ProviderStatus") + .HasColumnType("text"); + + b.Property("ProviderTaskId") + .HasColumnType("text"); + + b.Property("ProviderTaskUrl") + .HasColumnType("text"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Relations") + .HasColumnType("text"); + + b.Property("RemainingWork") + .HasColumnType("double precision"); + + b.Property("RevisedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Revision") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Severity") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoryPoints") + .HasColumnType("double precision"); + + b.Property("SystemFields") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TaskAssignDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskChangedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskCreateDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskDoneDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkItemType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToId"); + + b.HasIndex("CompletedById"); + + b.HasIndex("CreatedById1"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("Status"); + + b.HasIndex("TaskCreateDateTime"); + + b.ToTable("CrystaTasks", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CommentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("ContentHtml") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedByText") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaTaskId") + .HasColumnType("uuid"); + + b.Property("EditedById") + .HasColumnType("uuid"); + + b.Property("EditedByText") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("EditedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FormattedText") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("ParentCommentId") + .HasColumnType("integer"); + + b.Property("ProviderCommentId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderCommentUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ProviderTaskId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("Reactions") + .HasColumnType("text"); + + b.Property("Revision") + .IsRequired() + .HasColumnType("text"); + + b.Property("Text") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Visibility") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("CrystaTaskId"); + + b.HasIndex("EditedById"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("UserId"); + + b.ToTable("CrystaTaskComments", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AreaId") + .HasColumnType("uuid"); + + b.Property("AreaPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AssignedToId") + .HasColumnType("uuid"); + + b.Property("AssignedToText") + .HasColumnType("text"); + + b.Property("AttachmentsCount") + .HasColumnType("integer"); + + b.Property("BoardColumn") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BoardColumnDone") + .HasColumnType("boolean"); + + b.Property("ChangedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChangedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ChangedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommentCount") + .HasColumnType("integer"); + + b.Property("CompletedById") + .HasColumnType("uuid"); + + b.Property("CompletedByText") + .HasColumnType("text"); + + b.Property("CompletedWork") + .HasColumnType("double precision"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByDisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedByIdString") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedByText") + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromRevisionId") + .HasColumnType("integer"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaTaskId") + .HasColumnType("uuid"); + + b.Property("CustomFields") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DescriptionHtml") + .HasColumnType("text"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IterationId") + .HasColumnType("uuid"); + + b.Property("IterationPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Links") + .HasColumnType("text"); + + b.Property("OriginalEstimate") + .HasColumnType("double precision"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProjectName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProviderTaskId") + .HasColumnType("text"); + + b.Property("ProviderTaskUrl") + .HasColumnType("text"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Relations") + .HasColumnType("text"); + + b.Property("RemainingWork") + .HasColumnType("double precision"); + + b.Property("ResolvedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Revision") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Severity") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StateChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoryPoints") + .HasColumnType("double precision"); + + b.Property("SystemFields") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TaskAssignDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskCreateDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskDoneDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkItemType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToId"); + + b.HasIndex("ChangedDate"); + + b.HasIndex("CompletedById"); + + b.HasIndex("CreatedById"); + + b.HasIndex("CreatedDate"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("CrystaTaskId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("Revision"); + + b.HasIndex("State"); + + b.ToTable("CrystaTaskRevisions", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AttachmentChange") + .HasColumnType("text"); + + b.Property("ChangedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChangedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ChangedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ChangedPropertiesJson") + .HasColumnType("text"); + + b.Property("CommentText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaTaskId") + .HasColumnType("uuid"); + + b.Property("FieldDisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FieldName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsWorkItemFieldChange") + .HasColumnType("boolean"); + + b.Property("NewValue") + .HasColumnType("text"); + + b.Property("OldValue") + .HasColumnType("text"); + + b.Property("Operation") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderUpdateId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("RelationChange") + .HasColumnType("text"); + + b.Property("Revision") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChangedDate"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("CrystaTaskId"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("Revision"); + + b.HasIndex("UserId"); + + b.ToTable("CrystaTaskUpdates", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", "CrystaLearn"); + + b.HasData( + new + { + Id = new Guid("8ff71671-a1d6-5f97-abb9-d87d7b47d6e7"), + ConcurrencyStamp = "8ff71671-a1d6-5f97-abb9-d87d7b47d6e7", + Name = "s-admin", + NormalizedName = "S-ADMIN" + }, + new + { + Id = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8"), + ConcurrencyStamp = "9ff71672-a1d5-4f97-abb7-d87d6b47d5e8", + Name = "demo", + NormalizedName = "DEMO" + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId", "ClaimType", "ClaimValue"); + + b.ToTable("RoleClaims", "CrystaLearn"); + + b.HasData( + new + { + Id = 1, + ClaimType = "mx-p-s", + ClaimValue = "-1", + RoleId = new Guid("8ff71671-a1d6-5f97-abb9-d87d7b47d6e7") + }, + new + { + Id = 2, + ClaimType = "feat", + ClaimValue = "3.0", + RoleId = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8") + }, + new + { + Id = 3, + ClaimType = "feat", + ClaimValue = "3.1", + RoleId = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8") + }, + new + { + Id = 4, + ClaimType = "feat", + ClaimValue = "4.0", + RoleId = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8") + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("ElevatedAccessTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("HasProfilePicture") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OtpRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PhoneNumberTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetPasswordTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("TwoFactorTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("\"Email\" IS NOT NULL"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasFilter("\"PhoneNumber\" IS NOT NULL"); + + b.ToTable("Users", "CrystaLearn"); + + b.HasData( + new + { + Id = new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), + AccessFailedCount = 0, + BirthDate = new DateTimeOffset(new DateTime(2023, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + ConcurrencyStamp = "315e1a26-5b3a-4544-8e91-2760cd28e231", + Email = "test@bitplatform.dev", + EmailConfirmed = true, + EmailTokenRequestedOn = new DateTimeOffset(new DateTime(2023, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FullName = "CrystaLearn test account", + Gender = 0, + HasProfilePicture = false, + LockoutEnabled = true, + NormalizedEmail = "TEST@BITPLATFORM.DEV", + NormalizedUserName = "TEST", + PasswordHash = "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", + PhoneNumber = "+31684207362", + PhoneNumberConfirmed = true, + SecurityStamp = "959ff4a9-4b07-4cc1-8141-c5fc033daf83", + TwoFactorEnabled = false, + UserName = "test" + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ClaimType", "ClaimValue"); + + b.ToTable("UserClaims", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserRole", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId") + .IsUnique(); + + b.ToTable("UserRoles", "CrystaLearn"); + + b.HasData( + new + { + UserId = new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), + RoleId = new Guid("8ff71671-a1d6-5f97-abb9-d87d7b47d6e7") + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AppVersion") + .HasColumnType("text"); + + b.Property("CultureName") + .HasColumnType("text"); + + b.Property("DeviceInfo") + .HasColumnType("text"); + + b.Property("IP") + .HasColumnType("text"); + + b.Property("NotificationStatus") + .HasColumnType("integer"); + + b.Property("PlatformType") + .HasColumnType("integer"); + + b.Property("Privileged") + .HasColumnType("boolean"); + + b.Property("RenewedOn") + .HasColumnType("bigint"); + + b.Property("SignalRConnectionId") + .HasColumnType("text"); + + b.Property("StartedOn") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("bytea"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("AttestationClientDataJson") + .HasColumnType("bytea"); + + b.Property("AttestationFormat") + .HasColumnType("text"); + + b.Property("AttestationObject") + .HasColumnType("bytea"); + + b.Property("IsBackedUp") + .HasColumnType("boolean"); + + b.Property("IsBackupEligible") + .HasColumnType("boolean"); + + b.Property("PublicKey") + .HasColumnType("bytea"); + + b.Property("RegDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignCount") + .HasColumnType("bigint"); + + b.PrimitiveCollection("Transports") + .HasColumnType("integer[]"); + + b.Property("UserHandle") + .HasColumnType("bytea"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.PushNotification.PushNotificationSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Auth") + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Endpoint") + .HasColumnType("text"); + + b.Property("ExpirationTime") + .HasColumnType("bigint"); + + b.Property("P256dh") + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("PushChannel") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenewedOn") + .HasColumnType("bigint"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserSessionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserSessionId") + .IsUnique() + .HasFilter("\"UserSessionId\" IS NOT NULL"); + + b.ToTable("PushNotificationSubscriptions", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserToken", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "CrystaLearn"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Value"); + + b.ToTable("HangfireCounter", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Field") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Field"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireHash", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvocationData") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("bigint"); + + b.Property("StateName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("StateId"); + + b.HasIndex("StateName"); + + b.ToTable("HangfireJob", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("JobId", "Name"); + + b.ToTable("HangfireJobParameter", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Position"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireList", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("HangfireLock", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FetchedAt") + .IsConcurrencyToken() + .HasColumnType("timestamp with time zone"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Queue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("Queue", "FetchedAt"); + + b.ToTable("HangfireQueuedJob", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Heartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("Queues") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Heartbeat"); + + b.ToTable("HangfireServer", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.HasKey("Key", "Value"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Score"); + + b.ToTable("HangfireSet", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Reason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("HangfireState", "jobs"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaDocument", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaDocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaDocumentId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaDocument", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaDocumentId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("SyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgramSyncModule", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaProgramSyncModuleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaProgramSyncModuleId"); + + b1.ToTable("CrystaProgramSyncModules", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaProgramSyncModuleId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("SyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTask", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "AssignedTo") + .WithMany() + .HasForeignKey("AssignedToId"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CompletedBy") + .WithMany() + .HasForeignKey("CompletedById"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById1"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "CommentsSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "RevisionsSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "UpdatesSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "WorkItemSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.Navigation("AssignedTo"); + + b.Navigation("CommentsSyncInfo") + .IsRequired(); + + b.Navigation("CompletedBy"); + + b.Navigation("CreatedBy"); + + b.Navigation("CrystaProgram"); + + b.Navigation("Parent"); + + b.Navigation("RevisionsSyncInfo") + .IsRequired(); + + b.Navigation("UpdatesSyncInfo") + .IsRequired(); + + b.Navigation("WorkItemSyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskComment", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "CrystaTask") + .WithMany() + .HasForeignKey("CrystaTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "EditedBy") + .WithMany() + .HasForeignKey("EditedById"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaTaskCommentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskCommentId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTaskComments", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskCommentId"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("CrystaProgram"); + + b.Navigation("CrystaTask"); + + b.Navigation("EditedBy"); + + b.Navigation("SyncInfo"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskRevision", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "AssignedTo") + .WithMany() + .HasForeignKey("AssignedToId"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CompletedBy") + .WithMany() + .HasForeignKey("CompletedById"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "CrystaTask") + .WithMany() + .HasForeignKey("CrystaTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "CommentsSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "RevisionsSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "UpdatesSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "WorkItemSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.Navigation("AssignedTo"); + + b.Navigation("CommentsSyncInfo") + .IsRequired(); + + b.Navigation("CompletedBy"); + + b.Navigation("CreatedBy"); + + b.Navigation("CrystaProgram"); + + b.Navigation("CrystaTask"); + + b.Navigation("RevisionsSyncInfo") + .IsRequired(); + + b.Navigation("UpdatesSyncInfo") + .IsRequired(); + + b.Navigation("WorkItemSyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskUpdate", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "CrystaTask") + .WithMany() + .HasForeignKey("CrystaTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaTaskUpdateId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskUpdateId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTaskUpdates", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskUpdateId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("CrystaTask"); + + b.Navigation("SyncInfo"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.RoleClaim", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserClaim", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserRole", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserSession", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.WebAuthnCredential", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("WebAuthnCredentials") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.PushNotification.PushNotificationSubscription", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.UserSession", "UserSession") + .WithOne("PushNotificationSubscription") + .HasForeignKey("CrystaLearn.Core.Models.PushNotification.PushNotificationSubscription", "UserSessionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("UserSession"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserLogin", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserToken", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State") + .WithMany() + .HasForeignKey("StateId"); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("Parameters") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("QueuedJobs") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("States") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Roles"); + + b.Navigation("Sessions"); + + b.Navigation("Tokens"); + + b.Navigation("WebAuthnCredentials"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserSession", b => + { + b.Navigation("PushNotificationSubscription"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Navigation("Parameters"); + + b.Navigation("QueuedJobs"); + + b.Navigation("States"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Core/CrystaLearn.Core/Migrations/20251220121952_AddCrystaDocument.cs b/src/Core/CrystaLearn.Core/Migrations/20251220121952_AddCrystaDocument.cs new file mode 100644 index 00000000..c113c7a0 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Migrations/20251220121952_AddCrystaDocument.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CrystaLearn.Core.Migrations +{ + /// + public partial class AddCrystaDocument : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CrystaDocument", + schema: "CrystaLearn", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"), + Code = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Culture = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + Content = table.Column(type: "text", nullable: true), + SourceHtmlUrl = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + SourceContentUrl = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + CrystaUrl = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + Folder = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + FileName = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + LastHash = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + SyncInfo_SyncId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + SyncInfo_SyncStartDateTime = table.Column(type: "timestamp with time zone", nullable: true), + SyncInfo_SyncEndDateTime = table.Column(type: "timestamp with time zone", nullable: true), + SyncInfo_ContentHash = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + SyncInfo_SyncGroup = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + SyncInfo_SyncStatus = table.Column(type: "integer", nullable: true), + SyncInfo_LastSyncDateTime = table.Column(type: "timestamp with time zone", nullable: true), + SyncInfo_LastSyncOffset = table.Column(type: "character varying(40)", maxLength: 40, nullable: true), + CrystaProgramId = table.Column(type: "uuid", nullable: true), + FileExtension = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + FileNameWithoutExtension = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + DocumentType = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CrystaDocument", x => x.Id); + table.ForeignKey( + name: "FK_CrystaDocument_CrystaPrograms_CrystaProgramId", + column: x => x.CrystaProgramId, + principalSchema: "CrystaLearn", + principalTable: "CrystaPrograms", + principalColumn: "Id"); + }); + + //migrationBuilder.CreateIndex( + // name: "IX_CrystaDocument_Code", + // schema: "CrystaLearn", + // table: "CrystaDocument", + // column: "Code", + // unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CrystaDocument_CrystaProgramId", + schema: "CrystaLearn", + table: "CrystaDocument", + column: "CrystaProgramId"); + + migrationBuilder.CreateIndex( + name: "IX_CrystaDocument_IsActive", + schema: "CrystaLearn", + table: "CrystaDocument", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_CrystaDocument_SyncInfo_SyncId", + schema: "CrystaLearn", + table: "CrystaDocument", + column: "SyncInfo_SyncId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CrystaDocument", + schema: "CrystaLearn"); + } + } +} diff --git a/src/Core/CrystaLearn.Core/Migrations/20251226123528_AddDisplayCodeToCrytaDocument.Designer.cs b/src/Core/CrystaLearn.Core/Migrations/20251226123528_AddDisplayCodeToCrytaDocument.Designer.cs new file mode 100644 index 00000000..d2277c0e --- /dev/null +++ b/src/Core/CrystaLearn.Core/Migrations/20251226123528_AddDisplayCodeToCrytaDocument.Designer.cs @@ -0,0 +1,2529 @@ +// +using System; +using CrystaLearn.Core.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CrystaLearn.Core.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251226123528_AddDisplayCodeToCrytaDocument")] + partial class AddDisplayCodeToCrytaDocument + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("CrystaLearn") + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CrystaLearn.Core.Models.Attachments.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id", "Kind"); + + b.ToTable("Attachments", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Chatbot.SystemPrompt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("text"); + + b.Property("PromptKind") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PromptKind") + .IsUnique(); + + b.ToTable("SystemPrompts", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("DisplayCode") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("FileExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FileName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("FileNameWithoutExtension") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Folder") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SourceContentUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SourceHtmlUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("IsActive"); + + b.ToTable("CrystaDocument", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgram", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("BadgeSystemUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.ToTable("CrystaPrograms", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgramSyncModule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("ModuleType") + .HasColumnType("integer"); + + b.Property("SyncConfig") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("ModuleType"); + + b.ToTable("CrystaProgramSyncModules", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AreaId") + .HasColumnType("uuid"); + + b.Property("AreaPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AssignedToId") + .HasColumnType("uuid"); + + b.Property("AssignedToText") + .HasColumnType("text"); + + b.Property("AttachmentsCount") + .HasColumnType("integer"); + + b.Property("BoardColumn") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BoardColumnDone") + .HasColumnType("boolean"); + + b.Property("ChangedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChangedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CommentCount") + .HasColumnType("integer"); + + b.Property("CompletedById") + .HasColumnType("uuid"); + + b.Property("CompletedByText") + .HasColumnType("text"); + + b.Property("CompletedWork") + .HasColumnType("double precision"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedById1") + .HasColumnType("uuid"); + + b.Property("CreatedByText") + .HasColumnType("text"); + + b.Property("CreatedFromRevisionId") + .HasColumnType("integer"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CustomFields") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DescriptionHtml") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IterationId") + .HasColumnType("uuid"); + + b.Property("IterationPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Links") + .HasColumnType("text"); + + b.Property("OriginalEstimate") + .HasColumnType("double precision"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProjectName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProviderParentId") + .HasColumnType("text"); + + b.Property("ProviderStatus") + .HasColumnType("text"); + + b.Property("ProviderTaskId") + .HasColumnType("text"); + + b.Property("ProviderTaskUrl") + .HasColumnType("text"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Relations") + .HasColumnType("text"); + + b.Property("RemainingWork") + .HasColumnType("double precision"); + + b.Property("RevisedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Revision") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Severity") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoryPoints") + .HasColumnType("double precision"); + + b.Property("SystemFields") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TaskAssignDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskChangedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskCreateDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskDoneDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkItemType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToId"); + + b.HasIndex("CompletedById"); + + b.HasIndex("CreatedById1"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("Status"); + + b.HasIndex("TaskCreateDateTime"); + + b.ToTable("CrystaTasks", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CommentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("ContentHtml") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedByText") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaTaskId") + .HasColumnType("uuid"); + + b.Property("EditedById") + .HasColumnType("uuid"); + + b.Property("EditedByText") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("EditedDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FormattedText") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("ParentCommentId") + .HasColumnType("integer"); + + b.Property("ProviderCommentId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderCommentUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ProviderTaskId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("Reactions") + .HasColumnType("text"); + + b.Property("Revision") + .IsRequired() + .HasColumnType("text"); + + b.Property("Text") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Visibility") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("CrystaTaskId"); + + b.HasIndex("EditedById"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("UserId"); + + b.ToTable("CrystaTaskComments", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AreaId") + .HasColumnType("uuid"); + + b.Property("AreaPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AssignedToId") + .HasColumnType("uuid"); + + b.Property("AssignedToText") + .HasColumnType("text"); + + b.Property("AttachmentsCount") + .HasColumnType("integer"); + + b.Property("BoardColumn") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BoardColumnDone") + .HasColumnType("boolean"); + + b.Property("ChangedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChangedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ChangedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommentCount") + .HasColumnType("integer"); + + b.Property("CompletedById") + .HasColumnType("uuid"); + + b.Property("CompletedByText") + .HasColumnType("text"); + + b.Property("CompletedWork") + .HasColumnType("double precision"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByDisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedByIdString") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedByText") + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedFromRevisionId") + .HasColumnType("integer"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaTaskId") + .HasColumnType("uuid"); + + b.Property("CustomFields") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DescriptionHtml") + .HasColumnType("text"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IterationId") + .HasColumnType("uuid"); + + b.Property("IterationPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Links") + .HasColumnType("text"); + + b.Property("OriginalEstimate") + .HasColumnType("double precision"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProjectName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProviderTaskId") + .HasColumnType("text"); + + b.Property("ProviderTaskUrl") + .HasColumnType("text"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Relations") + .HasColumnType("text"); + + b.Property("RemainingWork") + .HasColumnType("double precision"); + + b.Property("ResolvedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Revision") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Severity") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StateChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoryPoints") + .HasColumnType("double precision"); + + b.Property("SystemFields") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TaskAssignDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskCreateDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskDoneDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkItemType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToId"); + + b.HasIndex("ChangedDate"); + + b.HasIndex("CompletedById"); + + b.HasIndex("CreatedById"); + + b.HasIndex("CreatedDate"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("CrystaTaskId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("Revision"); + + b.HasIndex("State"); + + b.ToTable("CrystaTaskRevisions", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AttachmentChange") + .HasColumnType("text"); + + b.Property("ChangedBy") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ChangedById") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ChangedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ChangedPropertiesJson") + .HasColumnType("text"); + + b.Property("CommentText") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaTaskId") + .HasColumnType("uuid"); + + b.Property("FieldDisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FieldName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsWorkItemFieldChange") + .HasColumnType("boolean"); + + b.Property("NewValue") + .HasColumnType("text"); + + b.Property("OldValue") + .HasColumnType("text"); + + b.Property("Operation") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderUpdateId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProviderUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RawJson") + .HasColumnType("text"); + + b.Property("RelationChange") + .HasColumnType("text"); + + b.Property("Revision") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChangedDate"); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("CrystaTaskId"); + + b.HasIndex("ProviderTaskId"); + + b.HasIndex("Revision"); + + b.HasIndex("UserId"); + + b.ToTable("CrystaTaskUpdates", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", "CrystaLearn"); + + b.HasData( + new + { + Id = new Guid("8ff71671-a1d6-5f97-abb9-d87d7b47d6e7"), + ConcurrencyStamp = "8ff71671-a1d6-5f97-abb9-d87d7b47d6e7", + Name = "s-admin", + NormalizedName = "S-ADMIN" + }, + new + { + Id = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8"), + ConcurrencyStamp = "9ff71672-a1d5-4f97-abb7-d87d6b47d5e8", + Name = "demo", + NormalizedName = "DEMO" + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId", "ClaimType", "ClaimValue"); + + b.ToTable("RoleClaims", "CrystaLearn"); + + b.HasData( + new + { + Id = 1, + ClaimType = "mx-p-s", + ClaimValue = "-1", + RoleId = new Guid("8ff71671-a1d6-5f97-abb9-d87d7b47d6e7") + }, + new + { + Id = 2, + ClaimType = "feat", + ClaimValue = "3.0", + RoleId = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8") + }, + new + { + Id = 3, + ClaimType = "feat", + ClaimValue = "3.1", + RoleId = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8") + }, + new + { + Id = 4, + ClaimType = "feat", + ClaimValue = "4.0", + RoleId = new Guid("9ff71672-a1d5-4f97-abb7-d87d6b47d5e8") + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("ElevatedAccessTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("HasProfilePicture") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OtpRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PhoneNumberTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetPasswordTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("TwoFactorTokenRequestedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("\"Email\" IS NOT NULL"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasFilter("\"PhoneNumber\" IS NOT NULL"); + + b.ToTable("Users", "CrystaLearn"); + + b.HasData( + new + { + Id = new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), + AccessFailedCount = 0, + BirthDate = new DateTimeOffset(new DateTime(2023, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + ConcurrencyStamp = "315e1a26-5b3a-4544-8e91-2760cd28e231", + Email = "test@bitplatform.dev", + EmailConfirmed = true, + EmailTokenRequestedOn = new DateTimeOffset(new DateTime(2023, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FullName = "CrystaLearn test account", + Gender = 0, + HasProfilePicture = false, + LockoutEnabled = true, + NormalizedEmail = "TEST@BITPLATFORM.DEV", + NormalizedUserName = "TEST", + PasswordHash = "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", + PhoneNumber = "+31684207362", + PhoneNumberConfirmed = true, + SecurityStamp = "959ff4a9-4b07-4cc1-8141-c5fc033daf83", + TwoFactorEnabled = false, + UserName = "test" + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ClaimType", "ClaimValue"); + + b.ToTable("UserClaims", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserRole", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId") + .IsUnique(); + + b.ToTable("UserRoles", "CrystaLearn"); + + b.HasData( + new + { + UserId = new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), + RoleId = new Guid("8ff71671-a1d6-5f97-abb9-d87d7b47d6e7") + }); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AppVersion") + .HasColumnType("text"); + + b.Property("CultureName") + .HasColumnType("text"); + + b.Property("DeviceInfo") + .HasColumnType("text"); + + b.Property("IP") + .HasColumnType("text"); + + b.Property("NotificationStatus") + .HasColumnType("integer"); + + b.Property("PlatformType") + .HasColumnType("integer"); + + b.Property("Privileged") + .HasColumnType("boolean"); + + b.Property("RenewedOn") + .HasColumnType("bigint"); + + b.Property("SignalRConnectionId") + .HasColumnType("text"); + + b.Property("StartedOn") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("bytea"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("AttestationClientDataJson") + .HasColumnType("bytea"); + + b.Property("AttestationFormat") + .HasColumnType("text"); + + b.Property("AttestationObject") + .HasColumnType("bytea"); + + b.Property("IsBackedUp") + .HasColumnType("boolean"); + + b.Property("IsBackupEligible") + .HasColumnType("boolean"); + + b.Property("PublicKey") + .HasColumnType("bytea"); + + b.Property("RegDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignCount") + .HasColumnType("bigint"); + + b.PrimitiveCollection("Transports") + .HasColumnType("integer[]"); + + b.Property("UserHandle") + .HasColumnType("bytea"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.PushNotification.PushNotificationSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Auth") + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Endpoint") + .HasColumnType("text"); + + b.Property("ExpirationTime") + .HasColumnType("bigint"); + + b.Property("P256dh") + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("PushChannel") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenewedOn") + .HasColumnType("bigint"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserSessionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserSessionId") + .IsUnique() + .HasFilter("\"UserSessionId\" IS NOT NULL"); + + b.ToTable("PushNotificationSubscriptions", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserToken", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "CrystaLearn"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireCounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Value"); + + b.ToTable("HangfireCounter", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireHash", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Field") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Field"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireHash", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvocationData") + .IsRequired() + .HasColumnType("text"); + + b.Property("StateId") + .HasColumnType("bigint"); + + b.Property("StateName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("StateId"); + + b.HasIndex("StateName"); + + b.ToTable("HangfireJob", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("JobId", "Name"); + + b.ToTable("HangfireJobParameter", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireList", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key", "Position"); + + b.HasIndex("ExpireAt"); + + b.ToTable("HangfireList", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireLock", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("HangfireLock", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FetchedAt") + .IsConcurrencyToken() + .HasColumnType("timestamp with time zone"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Queue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("Queue", "FetchedAt"); + + b.ToTable("HangfireQueuedJob", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireServer", b => + { + b.Property("Id") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Heartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("Queues") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Heartbeat"); + + b.ToTable("HangfireServer", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireSet", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.HasKey("Key", "Value"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("Key", "Score"); + + b.ToTable("HangfireSet", "jobs"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Reason") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.ToTable("HangfireState", "jobs"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys", "CrystaLearn"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaDocument", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaDocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaDocumentId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaDocument", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaDocumentId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("SyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgramSyncModule", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaProgramSyncModuleId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaProgramSyncModuleId"); + + b1.ToTable("CrystaProgramSyncModules", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaProgramSyncModuleId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("SyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTask", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "AssignedTo") + .WithMany() + .HasForeignKey("AssignedToId"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CompletedBy") + .WithMany() + .HasForeignKey("CompletedById"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById1"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "CommentsSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "RevisionsSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "UpdatesSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "WorkItemSyncInfo", b1 => + { + b1.Property("CrystaTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTasks", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskId"); + }); + + b.Navigation("AssignedTo"); + + b.Navigation("CommentsSyncInfo") + .IsRequired(); + + b.Navigation("CompletedBy"); + + b.Navigation("CreatedBy"); + + b.Navigation("CrystaProgram"); + + b.Navigation("Parent"); + + b.Navigation("RevisionsSyncInfo") + .IsRequired(); + + b.Navigation("UpdatesSyncInfo") + .IsRequired(); + + b.Navigation("WorkItemSyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskComment", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "CrystaTask") + .WithMany() + .HasForeignKey("CrystaTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "EditedBy") + .WithMany() + .HasForeignKey("EditedById"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaTaskCommentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskCommentId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTaskComments", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskCommentId"); + }); + + b.Navigation("CreatedBy"); + + b.Navigation("CrystaProgram"); + + b.Navigation("CrystaTask"); + + b.Navigation("EditedBy"); + + b.Navigation("SyncInfo"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskRevision", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "AssignedTo") + .WithMany() + .HasForeignKey("AssignedToId"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CompletedBy") + .WithMany() + .HasForeignKey("CompletedById"); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "CrystaTask") + .WithMany() + .HasForeignKey("CrystaTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "CommentsSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "RevisionsSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "UpdatesSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "WorkItemSyncInfo", b1 => + { + b1.Property("CrystaTaskRevisionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskRevisionId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTaskRevisions", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskRevisionId"); + }); + + b.Navigation("AssignedTo"); + + b.Navigation("CommentsSyncInfo") + .IsRequired(); + + b.Navigation("CompletedBy"); + + b.Navigation("CreatedBy"); + + b.Navigation("CrystaProgram"); + + b.Navigation("CrystaTask"); + + b.Navigation("RevisionsSyncInfo") + .IsRequired(); + + b.Navigation("UpdatesSyncInfo") + .IsRequired(); + + b.Navigation("WorkItemSyncInfo") + .IsRequired(); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaTaskUpdate", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaTask", "CrystaTask") + .WithMany() + .HasForeignKey("CrystaTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaTaskUpdateId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaTaskUpdateId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaTaskUpdates", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaTaskUpdateId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("CrystaTask"); + + b.Navigation("SyncInfo"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.RoleClaim", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserClaim", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserRole", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserSession", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.WebAuthnCredential", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("WebAuthnCredentials") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.PushNotification.PushNotificationSubscription", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.UserSession", "UserSession") + .WithOne("PushNotificationSubscription") + .HasForeignKey("CrystaLearn.Core.Models.PushNotification.PushNotificationSubscription", "UserSessionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("UserSession"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserLogin", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CrystaLearn.Server.Api.Models.Identity.UserToken", b => + { + b.HasOne("CrystaLearn.Core.Models.Identity.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireState", "State") + .WithMany() + .HasForeignKey("StateId"); + + b.Navigation("State"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJobParameter", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("Parameters") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireQueuedJob", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("QueuedJobs") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireState", b => + { + b.HasOne("Hangfire.EntityFrameworkCore.HangfireJob", "Job") + .WithMany("States") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Roles"); + + b.Navigation("Sessions"); + + b.Navigation("Tokens"); + + b.Navigation("WebAuthnCredentials"); + }); + + modelBuilder.Entity("CrystaLearn.Core.Models.Identity.UserSession", b => + { + b.Navigation("PushNotificationSubscription"); + }); + + modelBuilder.Entity("Hangfire.EntityFrameworkCore.HangfireJob", b => + { + b.Navigation("Parameters"); + + b.Navigation("QueuedJobs"); + + b.Navigation("States"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Core/CrystaLearn.Core/Migrations/20251226123528_AddDisplayCodeToCrytaDocument.cs b/src/Core/CrystaLearn.Core/Migrations/20251226123528_AddDisplayCodeToCrytaDocument.cs new file mode 100644 index 00000000..06e604dd --- /dev/null +++ b/src/Core/CrystaLearn.Core/Migrations/20251226123528_AddDisplayCodeToCrytaDocument.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CrystaLearn.Core.Migrations +{ + /// + public partial class AddDisplayCodeToCrytaDocument : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Code", + schema: "CrystaLearn", + table: "CrystaDocument", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(150)", + oldMaxLength: 150); + + migrationBuilder.AddColumn( + name: "DisplayCode", + schema: "CrystaLearn", + table: "CrystaDocument", + type: "character varying(150)", + maxLength: 150, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DisplayCode", + schema: "CrystaLearn", + table: "CrystaDocument"); + + migrationBuilder.AlterColumn( + name: "Code", + schema: "CrystaLearn", + table: "CrystaDocument", + type: "character varying(150)", + maxLength: 150, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/src/Core/CrystaLearn.Core/Migrations/AppDbContextModelSnapshot.cs b/src/Core/CrystaLearn.Core/Migrations/AppDbContextModelSnapshot.cs index 941f4073..d900955c 100644 --- a/src/Core/CrystaLearn.Core/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Core/CrystaLearn.Core/Migrations/AppDbContextModelSnapshot.cs @@ -69,6 +69,94 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SystemPrompts", "CrystaLearn"); }); + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrystaProgramId") + .HasColumnType("uuid"); + + b.Property("CrystaUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("DisplayCode") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("DocumentType") + .HasColumnType("integer"); + + b.Property("FileExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FileName") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("FileNameWithoutExtension") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Folder") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SourceContentUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("SourceHtmlUrl") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CrystaProgramId"); + + b.HasIndex("IsActive"); + + b.ToTable("CrystaDocument", "CrystaLearn"); + }); + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgram", b => { b.Property("Id") @@ -1549,6 +1637,64 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DataProtectionKeys", "CrystaLearn"); }); + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaDocument", b => + { + b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") + .WithMany() + .HasForeignKey("CrystaProgramId"); + + b.OwnsOne("CrystaLearn.Core.Models.Crysta.SyncInfo", "SyncInfo", b1 => + { + b1.Property("CrystaDocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b1.Property("ContentHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("LastSyncDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastSyncOffset") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b1.Property("SyncEndDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncGroup") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b1.Property("SyncStartDateTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("SyncStatus") + .HasColumnType("integer"); + + b1.HasKey("CrystaDocumentId"); + + b1.HasIndex("SyncId") + .IsUnique(); + + b1.ToTable("CrystaDocument", "CrystaLearn"); + + b1.WithOwner() + .HasForeignKey("CrystaDocumentId"); + }); + + b.Navigation("CrystaProgram"); + + b.Navigation("SyncInfo") + .IsRequired(); + }); + modelBuilder.Entity("CrystaLearn.Core.Models.Crysta.CrystaProgramSyncModule", b => { b.HasOne("CrystaLearn.Core.Models.Crysta.CrystaProgram", "CrystaProgram") diff --git a/src/Core/CrystaLearn.Core/Models/Crysta/Document.cs b/src/Core/CrystaLearn.Core/Models/Crysta/CrystaDocument.cs similarity index 89% rename from src/Core/CrystaLearn.Core/Models/Crysta/Document.cs rename to src/Core/CrystaLearn.Core/Models/Crysta/CrystaDocument.cs index 436a4d33..87c85a60 100644 --- a/src/Core/CrystaLearn.Core/Models/Crysta/Document.cs +++ b/src/Core/CrystaLearn.Core/Models/Crysta/CrystaDocument.cs @@ -3,10 +3,14 @@ namespace CrystaLearn.Core.Models.Crysta; -public class Document : Entity +[Table("CrystaDocument", Schema = "CrystaLearn")] +public class CrystaDocument : Entity { - [MaxLength(150)] public virtual string Code { get; set; } = default!; + + [MaxLength(150)] + public virtual string DisplayCode { get; set; } = default!; + [MaxLength(200)] public virtual string Title { get; set; } = default!; [MaxLength(10)] diff --git a/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs b/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs index 56b5d57a..1dfb3b4e 100644 --- a/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs +++ b/src/Core/CrystaLearn.Core/Services/Contracts/ICrystaProgramSyncModuleService.cs @@ -7,5 +7,5 @@ public interface ICrystaProgramSyncModuleService Task> GetSyncModulesAsync(CancellationToken cancellationToken); // Save or update a sync module (persist SyncInfo changes) - Task UpdateSyncModuleAsync(CrystaProgramSyncModule module); + Task UpdateSyncModuleAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default); } diff --git a/src/Core/CrystaLearn.Core/Services/Contracts/IDocumentRepository.cs b/src/Core/CrystaLearn.Core/Services/Contracts/IDocumentRepository.cs index 7ef79196..48ddbf21 100644 --- a/src/Core/CrystaLearn.Core/Services/Contracts/IDocumentRepository.cs +++ b/src/Core/CrystaLearn.Core/Services/Contracts/IDocumentRepository.cs @@ -5,7 +5,7 @@ namespace CrystaLearn.Core.Services.Contracts; public interface IDocumentRepository { - Task> GetDocumentsAsync(string programCode, CancellationToken cancellationToken); + Task> GetDocumentsAsync(string programCode, CancellationToken cancellationToken); Task GetDocumentByCrystaUrlAsync(string crystaUrl, string? culture, CancellationToken cancellationToken); diff --git a/src/Core/CrystaLearn.Core/Services/Contracts/IGithubSyncService.cs b/src/Core/CrystaLearn.Core/Services/Contracts/IGithubSyncService.cs new file mode 100644 index 00000000..45860014 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Services/Contracts/IGithubSyncService.cs @@ -0,0 +1,9 @@ +using CrystaLearn.Core.Models.Crysta; +using CrystaLearn.Core.Services.Sync; + +namespace CrystaLearn.Core.Services.Contracts; + +public interface IGitHubSyncService +{ + Task SyncAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default); +} diff --git a/src/Core/CrystaLearn.Core/Services/CrystaProgramService.cs b/src/Core/CrystaLearn.Core/Services/CrystaProgramService.cs new file mode 100644 index 00000000..f81601c7 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Services/CrystaProgramService.cs @@ -0,0 +1,25 @@ +using CrystaLearn.Core.Data; +using CrystaLearn.Core.Models.Crysta; +using CrystaLearn.Core.Services.Contracts; + +namespace CrystaLearn.Core.Services; + +public partial class CrystaProgramService : ICrystaProgramRepository +{ + [AutoInject] private AppDbContext DbContext { get; set; } = default!; + + public async Task> GetCrystaProgramsAsync(CancellationToken cancellationToken) + { + return await DbContext.CrystaPrograms + .Where(p => p.IsActive) + .OrderBy(p => p.Title) + .ToListAsync(cancellationToken); + } + + public async Task GetCrystaProgramByCodeAsync(string code, CancellationToken cancellationToken) + { + return await DbContext.CrystaPrograms + .Where(p => p.Code == code && p.IsActive) + .FirstOrDefaultAsync(cancellationToken); + } +} diff --git a/src/Core/CrystaLearn.Core/Services/CrystaProgramRepositoryFake.cs b/src/Core/CrystaLearn.Core/Services/CrystaProgramServiceFake.cs similarity index 93% rename from src/Core/CrystaLearn.Core/Services/CrystaProgramRepositoryFake.cs rename to src/Core/CrystaLearn.Core/Services/CrystaProgramServiceFake.cs index fd994293..64bd61d5 100644 --- a/src/Core/CrystaLearn.Core/Services/CrystaProgramRepositoryFake.cs +++ b/src/Core/CrystaLearn.Core/Services/CrystaProgramServiceFake.cs @@ -3,7 +3,7 @@ namespace CrystaLearn.Core.Services; -public partial class CrystaProgramRepositoryFake : ICrystaProgramRepository +public partial class CrystaProgramServiceFake : ICrystaProgramRepository { public static CrystaProgram FakeProgramCSI = new() { diff --git a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs index 6f3e8a16..c9c354b2 100644 --- a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs +++ b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleService.cs @@ -6,70 +6,102 @@ using CrystaLearn.Core.Data; using CrystaLearn.Core.Models.Crysta; using CrystaLearn.Core.Services.Contracts; +using Microsoft.EntityFrameworkCore; namespace CrystaLearn.Core.Services; -public partial class CrystaProgramSyncModuleService : ICrystaProgramSyncModuleService +public partial class CrystaProgramSyncModuleService : ICrystaProgramSyncModuleService, IDisposable { - private static List _modules = new(); - private AppDbContext DbContext { get; set; } = default!; + private List _modules = new(); + private IDbContextFactory DbContextFactory { get; set; } = default!; + private bool _initialized = false; + private readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _updateLock = new SemaphoreSlim(1, 1); + private bool _disposed = false; - public CrystaProgramSyncModuleService(AppDbContext dbContext) + public CrystaProgramSyncModuleService(IDbContextFactory dbContextFactory) { - this.DbContext = dbContext; - if (_modules.Count == 0) + this.DbContextFactory = dbContextFactory; + } + + private async Task EnsureInitializedAsync(CancellationToken cancellationToken) + { + if (!_initialized) { - _modules = DbContext.Set().ToListAsync().GetAwaiter().GetResult(); + await _initLock.WaitAsync(cancellationToken); + try + { + if (!_initialized) + { + await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken); + _modules = await dbContext.Set().Include(f => f.CrystaProgram).ToListAsync(cancellationToken); + _initialized = true; + } + } + finally + { + _initLock.Release(); + } } } public async Task> GetSyncModulesAsync(CancellationToken cancellationToken) { + await EnsureInitializedAsync(cancellationToken); return _modules; } - public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module) + public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default) { // Try to persist to database if DbContext is available and configured try { - if (DbContext != null) + if (DbContextFactory != null) { - var set = DbContext.Set(); + await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken); + var set = dbContext.Set(); - var existing = await set.FindAsync(new object[] { module.Id }, cancellationToken: CancellationToken.None); + var existing = await set.FindAsync(new object[] { module.Id }, cancellationToken: cancellationToken); if (existing != null) { // Update all scalar properties from incoming module - DbContext.Entry(existing).CurrentValues.SetValues(module); + dbContext.Entry(existing).CurrentValues.SetValues(module); // If SyncInfo is an owned/complex type, ensure its properties are updated as well if (module.SyncInfo != null) { existing.SyncInfo ??= new SyncInfo(); - DbContext.Entry(existing).CurrentValues.SetValues(existing); // ensure entry is tracked - DbContext.Entry(existing).Reference(e => e.SyncInfo).TargetEntry?.CurrentValues.SetValues(module.SyncInfo); + dbContext.Entry(existing).CurrentValues.SetValues(existing); // ensure entry is tracked + dbContext.Entry(existing).Reference(e => e.SyncInfo).TargetEntry?.CurrentValues.SetValues(module.SyncInfo); } - DbContext.Update(existing); + dbContext.Update(existing); } else { - await set.AddAsync(module); + await set.AddAsync(module, cancellationToken); } - await DbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); // keep in-memory copy in sync as well - replace whole object to reflect all fields - var idx = _modules.FindIndex(m => m.Id == module.Id); - if (idx >= 0) + await _updateLock.WaitAsync(cancellationToken); + try { - _modules[idx] = module; + var idx = _modules.FindIndex(m => m.Id == module.Id); + if (idx >= 0) + { + _modules[idx] = module; + } + else + { + _modules.Add(module); + } } - else + finally { - _modules.Add(module); + _updateLock.Release(); } return; @@ -81,14 +113,32 @@ public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module) } // Fallback: update in-memory collection (replace whole object) - var existingInMemoryIndex = _modules.FindIndex(m => m.Id == module.Id); - if (existingInMemoryIndex >= 0) + await _updateLock.WaitAsync(cancellationToken); + try + { + var existingInMemoryIndex = _modules.FindIndex(m => m.Id == module.Id); + if (existingInMemoryIndex >= 0) + { + _modules[existingInMemoryIndex] = module; + } + else + { + _modules.Add(module); + } + } + finally { - _modules[existingInMemoryIndex] = module; + _updateLock.Release(); } - else + } + + public void Dispose() + { + if (!_disposed) { - _modules.Add(module); + _initLock?.Dispose(); + _updateLock?.Dispose(); + _disposed = true; } } } diff --git a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs index 0870e209..5817979b 100644 --- a/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs +++ b/src/Core/CrystaLearn.Core/Services/CrystaProgramSyncModuleServiceFake.cs @@ -6,61 +6,100 @@ namespace CrystaLearn.Core.Services; -public partial class CrystaProgramSyncModuleServiceFake : ICrystaProgramSyncModuleService +public partial class CrystaProgramSyncModuleServiceFake : ICrystaProgramSyncModuleService, IDisposable { - private static List _modules = new(); + private List _modules = new(); + private bool _initialized = false; + private readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _updateLock = new SemaphoreSlim(1, 1); + private bool _disposed = false; private IConfiguration Configuration { get; set; } = default!; public CrystaProgramSyncModuleServiceFake(IConfiguration configuration) { Configuration = configuration; - if (_modules.Count == 0) - { - var pat = Configuration["AzureDevOps:PersonalAccessToken"]; + } - _modules = new List + private async Task EnsureInitializedAsync(CancellationToken cancellationToken = default) + { + if (!_initialized) + { + await _initLock.WaitAsync(cancellationToken); + try { - new CrystaProgramSyncModule + if (!_initialized) { - Id = Guid.NewGuid(), - CrystaProgramId = CrystaProgramRepositoryFake.FakeProgramCSI.Id, - CrystaProgram = CrystaProgramRepositoryFake.FakeProgramCSI, - ModuleType = SyncModuleType.AzureBoard, - SyncConfig = - $$""" + var pat = Configuration["AzureDevOps:PersonalAccessToken"]; + + _modules = new List + { + new CrystaProgramSyncModule + { + Id = Guid.NewGuid(), + CrystaProgramId = CrystaProgramServiceFake.FakeProgramCSI.Id, + CrystaProgram = CrystaProgramServiceFake.FakeProgramCSI, + ModuleType = SyncModuleType.AzureBoard, + SyncConfig = + $$""" + { + "Organization": "cs-internship", + "PersonalAccessToken": "{{pat}}", + "Project": "CS Internship Program" + } + """, + SyncInfo = new SyncInfo { - "Organization": "cs-internship", - "PersonalAccessToken": "{{pat}}", - "Project": "CS Internship Program" + LastSyncDateTime = DateTimeOffset.Now.AddDays(-2), + LastSyncOffset = "0" } - """, - SyncInfo = new SyncInfo - { - LastSyncDateTime = DateTimeOffset.Now.AddDays(-2), - LastSyncOffset = "0" - } + } + }; + _initialized = true; } - }; + } + finally + { + _initLock.Release(); + } } } public async Task> GetSyncModulesAsync(CancellationToken cancellationToken) { + await EnsureInitializedAsync(cancellationToken); return _modules; } - public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module) + public async Task UpdateSyncModuleAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default) { - var existing = _modules.FirstOrDefault(m => m.Id == module.Id); - if (existing != null) + await _updateLock.WaitAsync(cancellationToken); + try { - existing.SyncInfo = module.SyncInfo; - existing.SyncConfig = module.SyncConfig; + var existing = _modules.FirstOrDefault(m => m.Id == module.Id); + if (existing != null) + { + existing.SyncInfo = module.SyncInfo; + existing.SyncConfig = module.SyncConfig; + } + else + { + _modules.Add(module); + } + } + finally + { + _updateLock.Release(); } - else + } + + public void Dispose() + { + if (!_disposed) { - _modules.Add(module); + _initLock?.Dispose(); + _updateLock?.Dispose(); + _disposed = true; } } } diff --git a/src/Core/CrystaLearn.Core/Services/DocumentRepositoryDirectGitHub.cs b/src/Core/CrystaLearn.Core/Services/DocumentRepositoryDirectGitHub.cs index 001b2347..9a97298e 100644 --- a/src/Core/CrystaLearn.Core/Services/DocumentRepositoryDirectGitHub.cs +++ b/src/Core/CrystaLearn.Core/Services/DocumentRepositoryDirectGitHub.cs @@ -3,6 +3,7 @@ using CrystaLearn.Core.Services.GitHub; using CrystaLearn.Shared.Dtos.Crysta; using Markdig; +using CrystaLearn.Core.Mappers; namespace CrystaLearn.Core.Services; @@ -10,8 +11,9 @@ public partial class DocumentRepositoryDirectGitHub : IDocumentRepository { [AutoInject] private IGitHubService GitHubService { get; set; } = default; [AutoInject] private ICrystaProgramRepository CrystaProgramRepository { get; set; } = default; + [AutoInject] private CrystaLearn.Core.Services.Sync.ICrystaDocumentService CrystaDocumentService { get; set; } = default!; - public async Task> GetDocumentsAsync(string programCode, CancellationToken cancellationToken) + public async Task> GetDocumentsAsync(string programCode, CancellationToken cancellationToken) { var program = await CrystaProgramRepository.GetCrystaProgramByCodeAsync(programCode, cancellationToken); if (program == null) @@ -23,23 +25,55 @@ public async Task> GetDocumentsAsync(string programCode, Cancella var list = await GitHubService.GetFilesAsync(documentUrl); - var result = new List(); + var result = new List(); var culture = ""; foreach(var item in list) { var doc = item.CreateDocument(program); + if (!string.IsNullOrWhiteSpace(doc.SourceHtmlUrl)) + { + doc.Content ??= await GitHubService.GetFileContentAsync(doc.SourceHtmlUrl); + doc.Content = doc.GetHtmlContent(); + doc.Content = new string(doc.Content?.Where(c => c != '\0').ToArray()); + } result.Add(doc); } return result; } - public async Task GetDocumentByCrystaUrlAsync(string crystaUrl, + public async Task GetDocumentByCrystaUrlAsync(string crystaUrl, string? culture, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var languageVariants = await CrystaDocumentService.GetDocumentsByCrystaUrlAsync(crystaUrl, cancellationToken); + + if (languageVariants == null || !languageVariants.Any()) + { + return null; + } + + var document = languageVariants.FirstOrDefault(d => culture?.StartsWith(d.Culture) ?? false); + document ??= languageVariants.FirstOrDefault(d => d.Culture.StartsWith("en")); + document ??= languageVariants.FirstOrDefault(d => d.Culture.StartsWith("fa")); + + if (document is null) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(document.SourceHtmlUrl) && string.IsNullOrEmpty(document.Content)) + { + document.Content ??= await GitHubService.GetFileContentAsync(document.SourceHtmlUrl); + document.Content = document.GetHtmlContent(); + document.Content = new string(document.Content?.Where(c => c != '\0').ToArray()); + } + + var dto = document.Map(); + dto.CultureVariants = languageVariants.Select(x => x.Culture).ToArray(); + + return dto; } //public async Task GetDocumentContentByUrlAsync(string programCode, string url, diff --git a/src/Core/CrystaLearn.Core/Services/DocumentRepositoryInMemory.cs b/src/Core/CrystaLearn.Core/Services/DocumentRepositoryInMemory.cs index 50ad5099..82e3fe74 100644 --- a/src/Core/CrystaLearn.Core/Services/DocumentRepositoryInMemory.cs +++ b/src/Core/CrystaLearn.Core/Services/DocumentRepositoryInMemory.cs @@ -12,9 +12,9 @@ public partial class DocumentRepositoryInMemory : IDocumentRepository [AutoInject] private IGitHubService GitHubService { get; set; } = default; [AutoInject] private ICrystaProgramRepository CrystaProgramRepository { get; set; } = default; - private ConcurrentDictionary> ProgramDocs { get; set; } = new(); + private ConcurrentDictionary> ProgramDocs { get; set; } = new(); - public async Task> GetDocumentsAsync(string programCode, CancellationToken cancellationToken) + public async Task> GetDocumentsAsync(string programCode, CancellationToken cancellationToken) { if (ProgramDocs.TryGetValue(programCode, out var list)) { @@ -57,7 +57,7 @@ public async Task> GetDocumentsAsync(string programCode, Cancella - private async Task PopulateContentAsync(Document document) + private async Task PopulateContentAsync(CrystaDocument document) { if (string.IsNullOrWhiteSpace(document.SourceHtmlUrl)) { @@ -68,7 +68,7 @@ private async Task PopulateContentAsync(Document document) document.Content = document.GetHtmlContent(); } - private async Task> GetProgramDocsFromGitHubAsync(string programCode, CancellationToken cancellationToken) + private async Task> GetProgramDocsFromGitHubAsync(string programCode, CancellationToken cancellationToken) { var program = await CrystaProgramRepository.GetCrystaProgramByCodeAsync(programCode, cancellationToken); if (program == null) @@ -78,7 +78,7 @@ private async Task> GetProgramDocsFromGitHubAsync(string programC var list = program.DocumentUrl is not null ? await GitHubService.GetFilesAsync(program.DocumentUrl) : []; - var result = new List(); + var result = new List(); foreach (var item in list) { diff --git a/src/Core/CrystaLearn.Core/Services/GitHub/GitHubExtensions.cs b/src/Core/CrystaLearn.Core/Services/GitHub/GitHubExtensions.cs index b9e90c89..2cb3c090 100644 --- a/src/Core/CrystaLearn.Core/Services/GitHub/GitHubExtensions.cs +++ b/src/Core/CrystaLearn.Core/Services/GitHub/GitHubExtensions.cs @@ -1,11 +1,12 @@ -using CrystaLearn.Core.Models.Crysta; +using CrystaLearn.Core.Extensions; +using CrystaLearn.Core.Models.Crysta; using CrystaLearn.Shared; using Markdig; namespace CrystaLearn.Core.Services.GitHub; public static class GitHubExtensions { - public static Models.Crysta.Document CreateDocument(this GitHubItem item, CrystaProgram program) + public static Models.Crysta.CrystaDocument CreateDocument(this GitHubItem item, CrystaProgram program) { string culture = ""; string programDocUrl = program.DocumentUrl ?? throw new Exception($"Program with code '{program.Code}' has no document url."); @@ -50,10 +51,11 @@ public static Models.Crysta.Document CreateDocument(this GitHubItem item, Crysta var folderPath = relativePath; - var doc = new Models.Crysta.Document + var doc = new Models.Crysta.CrystaDocument { Id = Guid.NewGuid(), Code = code, + DisplayCode = code, Title = title, Culture = culture, Content = null, @@ -73,12 +75,13 @@ public static Models.Crysta.Document CreateDocument(this GitHubItem item, Crysta SyncStatus = SyncStatus.Success, ContentHash = item.Sha, SyncStartDateTime = DateTimeOffset.Now, + SyncId = item.HtmlUrl.Sha() } }; return doc; } - public static string? GetHtmlContent(this Models.Crysta.Document doc) + public static string? GetHtmlContent(this Models.Crysta.CrystaDocument doc) { var content = doc.Content; if (content == null) diff --git a/src/Core/CrystaLearn.Core/Services/Sync/CrystaDocumentService.cs b/src/Core/CrystaLearn.Core/Services/Sync/CrystaDocumentService.cs new file mode 100644 index 00000000..5532bdf7 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Services/Sync/CrystaDocumentService.cs @@ -0,0 +1,67 @@ +using CrystaLearn.Core.Models.Crysta; +using CrystaLearn.Core.Data; +using Microsoft.EntityFrameworkCore; + +namespace CrystaLearn.Core.Services.Sync; + +public partial class CrystaDocumentService : ICrystaDocumentService +{ + [AutoInject] private AppDbContext DbContext { get; set; } = default!; + + public async Task SaveDocumentsAsync( + IEnumerable newDocuments, + IEnumerable updatedDocuments, + IEnumerable deletedDocuments, + CancellationToken cancellationToken = default) + { + if (newDocuments != null && newDocuments.Any()) + { + await DbContext.CrystaDocument.AddRangeAsync(newDocuments); + } + + if (updatedDocuments != null && updatedDocuments.Any()) + { + // Updated documents may already be tracked; ensure they are marked modified + DbContext.CrystaDocument.UpdateRange(updatedDocuments); + } + + if (deletedDocuments != null && deletedDocuments.Any()) + { + foreach (var d in deletedDocuments) + { + d.IsActive = false; + d.UpdatedAt = DateTimeOffset.Now; + } + DbContext.CrystaDocument.UpdateRange(deletedDocuments); + } + + await DbContext.SaveChangesAsync(cancellationToken); + } + + public async Task> GetDocumentsByCrystaUrlAsync(string crystaUrl, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(crystaUrl)) + { + return new List(); + } + + return await DbContext.CrystaDocument + .AsNoTracking() + .Where(d => d.CrystaUrl == crystaUrl && d.IsActive) + .ToListAsync(cancellationToken); + } + + public async Task> GetDocumentsByProgramCodeAsync(string programCode, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(programCode)) + { + return new List(); + } + + return await DbContext.CrystaDocument + .AsNoTracking() + .Where(d => d.CrystaProgram != null && d.CrystaProgram.Code == programCode && d.IsActive) + .OrderBy(d => d.FileName) + .ToListAsync(cancellationToken); + } +} diff --git a/src/Core/CrystaLearn.Core/Services/Sync/CrystaDocumentServiceFake.cs b/src/Core/CrystaLearn.Core/Services/Sync/CrystaDocumentServiceFake.cs new file mode 100644 index 00000000..49145211 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Services/Sync/CrystaDocumentServiceFake.cs @@ -0,0 +1,116 @@ +using CrystaLearn.Core.Models.Crysta; + +namespace CrystaLearn.Core.Services.Sync; + +public class CrystaDocumentServiceFake : ICrystaDocumentService +{ + private readonly List _store = new(); + + public IReadOnlyCollection InMemoryDocuments => _store.AsReadOnly(); + + public Task SaveDocumentsAsync( + IEnumerable newDocuments, + IEnumerable updatedDocuments, + IEnumerable deletedDocuments, + CancellationToken cancellationToken = default) + { + if (newDocuments != null) + { + foreach (var d in newDocuments) + { + var copy = CloneDocument(d); + _store.Add(copy); + } + } + + if (updatedDocuments != null) + { + foreach (var d in updatedDocuments) + { + var key = GetKey(d); + var existing = _store.FirstOrDefault(x => GetKey(x) == key); + if (existing != null) + { + ApplyUpdate(existing, d); + } + else + { + _store.Add(CloneDocument(d)); + } + } + } + + if (deletedDocuments != null) + { + foreach (var d in deletedDocuments) + { + var key = GetKey(d); + var existing = _store.FirstOrDefault(x => GetKey(x) == key); + if (existing != null) + { + existing.IsActive = false; + existing.UpdatedAt = DateTimeOffset.Now; + } + } + } + + return Task.CompletedTask; + } + + private static string GetKey(CrystaDocument d) => $"{d.Code}_{d.Culture}"; + + private static CrystaDocument CloneDocument(CrystaDocument src) + { + return new CrystaDocument + { + Id = src.Id, + Code = src.Code, + DisplayCode = src.DisplayCode, + Culture = src.Culture, + Title = src.Title, + Content = src.Content, + SourceHtmlUrl = src.SourceHtmlUrl, + SourceContentUrl = src.SourceContentUrl, + CrystaUrl = src.CrystaUrl, + Folder = src.Folder, + FileName = src.FileName, + FileExtension = src.FileExtension, + FileNameWithoutExtension = src.FileNameWithoutExtension, + DocumentType = src.DocumentType, + LastHash = src.LastHash, + IsActive = src.IsActive, + CreatedAt = src.CreatedAt, + UpdatedAt = src.UpdatedAt, + CrystaProgramId = src.CrystaProgramId, + SyncInfo = src.SyncInfo // assuming SyncInfo is a reference type handled in tests + }; + } + + private static void ApplyUpdate(CrystaDocument existing, CrystaDocument src) + { + existing.Title = src.Title; + existing.Content = src.Content; + existing.SourceHtmlUrl = src.SourceHtmlUrl; + existing.SourceContentUrl = src.SourceContentUrl; + existing.CrystaUrl = src.CrystaUrl; + existing.Folder = src.Folder; + existing.FileName = src.FileName; + existing.FileExtension = src.FileExtension; + existing.FileNameWithoutExtension = src.FileNameWithoutExtension; + existing.DocumentType = src.DocumentType; + existing.LastHash = src.LastHash; + existing.IsActive = src.IsActive; + existing.SyncInfo = src.SyncInfo; + existing.UpdatedAt = DateTimeOffset.Now; + } + + public Task> GetDocumentsByCrystaUrlAsync(string crystaUrl, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task> GetDocumentsByProgramCodeAsync(string programCode, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/Core/CrystaLearn.Core/Services/Sync/CrystaProgramSyncService.cs b/src/Core/CrystaLearn.Core/Services/Sync/CrystaProgramSyncService.cs index 2639ecc5..7ec25af4 100644 --- a/src/Core/CrystaLearn.Core/Services/Sync/CrystaProgramSyncService.cs +++ b/src/Core/CrystaLearn.Core/Services/Sync/CrystaProgramSyncService.cs @@ -11,6 +11,7 @@ namespace CrystaLearn.Core.Services.Sync; public partial class CrystaProgramSyncService : ICrystaProgramSyncService { [AutoInject] private IAzureBoardSyncService AzureBoardSyncService { get; set; } = default!; + [AutoInject] private IGitHubSyncService GithubSyncService { get; set; } = default!; public async Task SyncAsync(CrystaProgramSyncModule module) { @@ -19,6 +20,9 @@ public async Task SyncAsync(CrystaProgramSyncModule module) case SyncModuleType.AzureBoard: await AzureBoardSyncService.SyncAsync(module); break; + case SyncModuleType.GitHubDocument: + await GithubSyncService.SyncAsync(module); + break; default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Core/CrystaLearn.Core/Services/Sync/GithubSyncService.cs b/src/Core/CrystaLearn.Core/Services/Sync/GithubSyncService.cs new file mode 100644 index 00000000..9b73e825 --- /dev/null +++ b/src/Core/CrystaLearn.Core/Services/Sync/GithubSyncService.cs @@ -0,0 +1,156 @@ +using CrystaLearn.Core.Data; +using CrystaLearn.Core.Extensions; +using CrystaLearn.Core.Models.Crysta; +using CrystaLearn.Core.Services.Contracts; + +namespace CrystaLearn.Core.Services.Sync; + +public partial class GitHubSyncService : IGitHubSyncService +{ + [AutoInject] private IDocumentRepository DocumentRepository { get; set; } = default!; + [AutoInject] private AppDbContext DbContext { get; set; } = default!; + [AutoInject] private ICrystaProgramSyncModuleService CrystaProgramSyncModuleService { get; set; } = default!; + [AutoInject] private ICrystaDocumentService CrystaDocumentService { get; set; } = default!; + + public async Task SyncAsync(CrystaProgramSyncModule module, CancellationToken cancellationToken = default) + { + if (module.ModuleType != SyncModuleType.GitHubDocument) + { + throw new InvalidOperationException("Invalid module type"); + } + + var program = module.CrystaProgram ?? throw new ArgumentNullException(nameof(module.CrystaProgram)); + var programCode = program.Code ?? throw new ArgumentNullException(nameof(program.Code)); + + module.SyncInfo.SyncStartDateTime = DateTimeOffset.Now; + + // Fetch documents from GitHub using DocumentRepositoryInMemory + var githubDocuments = await DocumentRepository.GetDocumentsAsync(programCode, cancellationToken); + + // Sync documents to database + var syncResult = await SyncDocumentsAsync(githubDocuments, program.Id, cancellationToken); + + // Update module sync info + module.SyncInfo.LastSyncDateTime = DateTimeOffset.Now; + module.SyncInfo.SyncEndDateTime = DateTimeOffset.Now; + module.SyncInfo.SyncStatus = SyncStatus.Success; + module.UpdatedAt = DateTimeOffset.Now; + + // Persist module sync info + await CrystaProgramSyncModuleService.UpdateSyncModuleAsync(module); + + return syncResult; + } + + private async Task SyncDocumentsAsync(List githubDocuments, Guid programId, CancellationToken cancellationToken) + { + var result = new SyncResult { AddCount = 0, UpdateCount = 0, SameCount = 0, DeleteCount = 0 }; + + // Get existing documents from database for this program + var existingDocuments = await DbContext.CrystaDocument + .Where(d => d.CrystaProgramId == programId) + .ToListAsync(cancellationToken); + + // Create a map of existing documents by their unique identifier (combination of code and culture) + // Filter out documents with null SyncId to prevent ArgumentNullException + var existingDocMap = existingDocuments + .Where(d => !string.IsNullOrWhiteSpace(d.SyncInfo.SyncId)) + .ToDictionary(d => d.SyncInfo.SyncId!, d => d); + + // Track which documents from GitHub we've processed + var processedKeys = new HashSet(); + + // Collect documents to add / update / delete and then save in batch + var newDocuments = new List(); + var updatedDocuments = new List(); + var deletedDocuments = new List(); + + foreach (var githubDoc in githubDocuments) + { + var key = githubDoc.SyncInfo.SyncId; + + // Skip documents with null SyncId to prevent adding null to HashSet + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + processedKeys.Add(key); + + if (existingDocMap.TryGetValue(key, out var existingDoc)) + { + // Document exists - check if it needs updating + if (existingDoc.LastHash != githubDoc.LastHash) + { + // Document has changed - update it + existingDoc.Title = githubDoc.Title; + existingDoc.Content = githubDoc.Content; + existingDoc.SourceHtmlUrl = githubDoc.SourceHtmlUrl; + existingDoc.SourceContentUrl = githubDoc.SourceContentUrl; + existingDoc.CrystaUrl = githubDoc.CrystaUrl; + existingDoc.Folder = githubDoc.Folder; + existingDoc.FileName = githubDoc.FileName; + existingDoc.FileExtension = githubDoc.FileExtension; + existingDoc.FileNameWithoutExtension = githubDoc.FileNameWithoutExtension; + existingDoc.DocumentType = githubDoc.DocumentType; + existingDoc.LastHash = githubDoc.LastHash; + existingDoc.IsActive = true; + existingDoc.SyncInfo.LastSyncDateTime = DateTimeOffset.Now; + existingDoc.SyncInfo.ContentHash = githubDoc.LastHash; + existingDoc.SyncInfo.SyncStatus = SyncStatus.Success; + existingDoc.UpdatedAt = DateTimeOffset.Now; + + result.UpdateCount++; + // Keep track to update in batch + updatedDocuments.Add(existingDoc); + } + else + { + // Document unchanged + result.SameCount++; + } + } + else + { + // New document - prepare to add it in batch + githubDoc.CrystaProgramId = programId; + githubDoc.CreatedAt = DateTimeOffset.Now; + githubDoc.UpdatedAt = DateTimeOffset.Now; + githubDoc.SyncInfo.LastSyncDateTime = DateTimeOffset.Now; + githubDoc.SyncInfo.SyncStatus = SyncStatus.Success; + githubDoc.SyncInfo.ContentHash = githubDoc.LastHash; + newDocuments.Add(githubDoc); + result.AddCount++; + } + } + + // Mark documents that are no longer in GitHub as inactive + foreach (var existingDoc in existingDocuments) + { + var key = existingDoc.SyncInfo.SyncId; + + // Skip documents with null SyncId to prevent checking null in HashSet + if (string.IsNullOrWhiteSpace(key)) + { + continue; + } + + if (!processedKeys.Contains(key) && existingDoc.IsActive) + { + existingDoc.IsActive = false; + existingDoc.UpdatedAt = DateTimeOffset.Now; + result.DeleteCount++; + deletedDocuments.Add(existingDoc); + } + } + + // Persist changes via the document service (batch save) + await CrystaDocumentService.SaveDocumentsAsync( + newDocuments, + updatedDocuments, + deletedDocuments, + cancellationToken); + + return result; + } +} diff --git a/src/Core/CrystaLearn.Core/Services/Sync/ICrystaDocumentService.cs b/src/Core/CrystaLearn.Core/Services/Sync/ICrystaDocumentService.cs new file mode 100644 index 00000000..bb50ee2e --- /dev/null +++ b/src/Core/CrystaLearn.Core/Services/Sync/ICrystaDocumentService.cs @@ -0,0 +1,15 @@ +using CrystaLearn.Core.Models.Crysta; +namespace CrystaLearn.Core.Services.Sync; + +public interface ICrystaDocumentService +{ + Task SaveDocumentsAsync( + IEnumerable newDocuments, + IEnumerable updatedDocuments, + IEnumerable deletedDocuments, + CancellationToken cancellationToken = default); + + Task> GetDocumentsByCrystaUrlAsync(string crystaUrl, CancellationToken cancellationToken = default); + + Task> GetDocumentsByProgramCodeAsync(string programCode, CancellationToken cancellationToken = default); +} diff --git a/src/Server/CrystaLearn.Server.Api/Controllers/Crysta/DocumentController.cs b/src/Server/CrystaLearn.Server.Api/Controllers/Crysta/DocumentController.cs index 1910845c..baeee19a 100644 --- a/src/Server/CrystaLearn.Server.Api/Controllers/Crysta/DocumentController.cs +++ b/src/Server/CrystaLearn.Server.Api/Controllers/Crysta/DocumentController.cs @@ -1,5 +1,5 @@ using System.Web; -using CrystaLearn.Core.Services.Contracts; +using CrystaLearn.Core.Services.Sync; using CrystaLearn.Shared.Controllers.Crysta; using CrystaLearn.Shared.Dtos.Crysta; @@ -8,14 +8,14 @@ namespace CrystaLearn.Server.Api.Controllers.Crysta; [ApiController, Route("api/[controller]/[action]")] public partial class DocumentController : AppControllerBase, IDocumentController { - [AutoInject] private IDocumentRepository DocumentRepository { get; set; } + [AutoInject] private ICrystaDocumentService CrystaDocumentService { get; set; } [AllowAnonymous] [HttpGet("{programCode}")] //[ResponseCache(Duration = 1 * 24 * 3600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = ["*"])] public async Task> GetDocuments(string programCode, CancellationToken cancellationToken) { - var list = await DocumentRepository.GetDocumentsAsync(programCode, cancellationToken); + var list = await CrystaDocumentService.GetDocumentsByProgramCodeAsync(programCode, cancellationToken); var dtos = list.Select(x => x.Map()).ToList(); dtos.ForEach(o => o.Content = null); return dtos; @@ -28,7 +28,22 @@ public async Task> GetDocuments(string programCode, Cancellati CancellationToken cancellationToken) { var decoded = HttpUtility.UrlDecode(crystaUrl); - var result = await DocumentRepository.GetDocumentByCrystaUrlAsync(decoded, culture, cancellationToken); - return result; + var docs = await CrystaDocumentService.GetDocumentsByCrystaUrlAsync(decoded, cancellationToken); + + var languageVariants = docs.Where(o => o.CrystaUrl == decoded).ToList(); + + var document = languageVariants.FirstOrDefault(d => culture?.StartsWith(d.Culture) ?? false); + document ??= languageVariants.FirstOrDefault(d => d.Culture.StartsWith("en")); + document ??= languageVariants.FirstOrDefault(d => d.Culture.StartsWith("fa")); + + if (document is null) + { + return null; + } + + var dto = document.Map(); + dto.CultureVariants = languageVariants.Select(x => x.Culture).ToArray(); + + return dto; } } diff --git a/src/Shared/CrystaLearn.Shared/Dtos/Crysta/SyncInfoDto.cs b/src/Shared/CrystaLearn.Shared/Dtos/Crysta/SyncInfoDto.cs index cfdf8649..dd27e0ec 100644 --- a/src/Shared/CrystaLearn.Shared/Dtos/Crysta/SyncInfoDto.cs +++ b/src/Shared/CrystaLearn.Shared/Dtos/Crysta/SyncInfoDto.cs @@ -7,7 +7,7 @@ namespace CrystaLearn.Shared.Dtos.Crysta; public class SyncInfoDto { - public Guid? SyncId { get; set; } + public string? SyncId { get; set; } public DateTimeOffset? SyncStartDateTime { get; set; } public DateTimeOffset? SyncEndDateTime { get; set; } [MaxLength(100)]