diff --git a/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs b/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs index 3e19633d7..1d77fe834 100644 --- a/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs +++ b/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs @@ -8,8 +8,5 @@ public class OrgGqlConfiguration : ObjectType protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Field(o => o.CreatedDate).IsProjected(); - // TODO: Will we want something similar to the following Project code for orgs? - // descriptor.Field(o => o.Id).Use(); - // descriptor.Field(o => o.Members).Use(); } } diff --git a/backend/LexBoxApi/GraphQL/CustomTypes/OrgProjectsGqlConfiguration.cs b/backend/LexBoxApi/GraphQL/CustomTypes/OrgProjectsGqlConfiguration.cs new file mode 100644 index 000000000..cbe188a96 --- /dev/null +++ b/backend/LexBoxApi/GraphQL/CustomTypes/OrgProjectsGqlConfiguration.cs @@ -0,0 +1,13 @@ +using LexCore.Entities; + +namespace LexBoxApi.GraphQL.CustomTypes; + +[ObjectType] +public class OrgProjectsGqlConfiguration : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(op => op.Org).Type>(); + descriptor.Field(op => op.Project).Type>(); + } +} diff --git a/backend/LexBoxApi/GraphQL/OrgMutations.cs b/backend/LexBoxApi/GraphQL/OrgMutations.cs index e9c8217d1..6d317f653 100644 --- a/backend/LexBoxApi/GraphQL/OrgMutations.cs +++ b/backend/LexBoxApi/GraphQL/OrgMutations.cs @@ -32,7 +32,8 @@ public async Task> CreateOrganization(string name, Members = [ new OrgMember() { Role = OrgRole.Admin, UserId = userId } - ] + ], + Projects = [] }); await dbContext.SaveChangesAsync(); return dbContext.Orgs.Where(o => o.Id == orgId); @@ -52,6 +53,69 @@ public async Task DeleteOrg(Guid orgId, return org; } + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> AddProjectToOrg( + LexBoxDbContext dbContext, + IPermissionService permissionService, + Guid orgId, + Guid projectId) + { + var org = await dbContext.Orgs.FindAsync(orgId); + NotFoundException.ThrowIfNull(org); + permissionService.AssertCanAddProjectToOrg(org); + var project = await dbContext.Projects.Where(p => p.Id == projectId) + .Include(p => p.Organizations) + .SingleOrDefaultAsync(); + NotFoundException.ThrowIfNull(project); + permissionService.AssertCanManageProject(projectId); + + if (project.Organizations.Exists(o => o.Id == orgId)) + { + // No error since we're already in desired state; just return early + return dbContext.Orgs.Where(o => o.Id == orgId); + } + project.Organizations.Add(org); + project.UpdateUpdatedDate(); + org.UpdateUpdatedDate(); + await dbContext.SaveChangesAsync(); + return dbContext.Orgs.Where(o => o.Id == orgId); + } + + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> RemoveProjectFromOrg( + LexBoxDbContext dbContext, + IPermissionService permissionService, + Guid orgId, + Guid projectId) + { + var org = await dbContext.Orgs.FindAsync(orgId); + NotFoundException.ThrowIfNull(org); + permissionService.AssertCanAddProjectToOrg(org); + var project = await dbContext.Projects.Where(p => p.Id == projectId) + .Include(p => p.Organizations) + .SingleOrDefaultAsync(); + NotFoundException.ThrowIfNull(project); + permissionService.AssertCanManageProject(projectId); + var foundOrg = project.Organizations.FirstOrDefault(o => o.Id == orgId); + if (foundOrg is not null) + { + project.Organizations.Remove(foundOrg); + project.UpdateUpdatedDate(); + org.UpdateUpdatedDate(); + await dbContext.SaveChangesAsync(); + } + // If org did not own project, return with no error + return dbContext.Orgs.Where(o => o.Id == orgId); + } + /// /// set the role of a member in an organization, if the member does not exist it will be created /// diff --git a/backend/LexBoxApi/Models/Project/CreateProjectInput.cs b/backend/LexBoxApi/Models/Project/CreateProjectInput.cs index e97c6720d..9e942925f 100644 --- a/backend/LexBoxApi/Models/Project/CreateProjectInput.cs +++ b/backend/LexBoxApi/Models/Project/CreateProjectInput.cs @@ -12,5 +12,6 @@ public record CreateProjectInput( ProjectType Type, RetentionPolicy RetentionPolicy, bool IsConfidential, + Guid? OwningOrgId, Guid? ProjectManagerId ); diff --git a/backend/LexBoxApi/Services/PermissionService.cs b/backend/LexBoxApi/Services/PermissionService.cs index e394c6eab..9f0994f12 100644 --- a/backend/LexBoxApi/Services/PermissionService.cs +++ b/backend/LexBoxApi/Services/PermissionService.cs @@ -108,4 +108,12 @@ public void AssertCanEditOrg(Organization org) if (org.Members.Any(m => m.UserId == User.Id && m.Role == OrgRole.Admin)) return; throw new UnauthorizedAccessException(); } + + public void AssertCanAddProjectToOrg(Organization org) + { + if (User is null) throw new UnauthorizedAccessException(); + if (User.Role == UserRole.admin) return; + if (org.Members.Any(m => m.UserId == User.Id)) return; + throw new UnauthorizedAccessException(); + } } diff --git a/backend/LexBoxApi/Services/ProjectService.cs b/backend/LexBoxApi/Services/ProjectService.cs index 826b10a03..ccb3bae3d 100644 --- a/backend/LexBoxApi/Services/ProjectService.cs +++ b/backend/LexBoxApi/Services/ProjectService.cs @@ -35,6 +35,7 @@ public async Task CreateProject(CreateProjectInput input) LastCommit = null, RetentionPolicy = input.RetentionPolicy, IsConfidential = isConfidentialIsUntrustworthy ? null : input.IsConfidential, + Organizations = [], Users = input.ProjectManagerId.HasValue ? [new() { UserId = input.ProjectManagerId.Value, Role = ProjectRole.Manager }] : [], }); // Also delete draft project, if any @@ -48,6 +49,12 @@ public async Task CreateProject(CreateProjectInput input) await emailService.SendApproveProjectRequestEmail(manager, input); } } + if (input.OwningOrgId.HasValue) + { + dbContext.OrgProjects.Add( + new OrgProjects { ProjectId = projectId, OrgId = input.OwningOrgId.Value } + ); + } await dbContext.SaveChangesAsync(); await hgService.InitRepo(input.Code); await transaction.CommitAsync(); diff --git a/backend/LexCore/Entities/OrgProjects.cs b/backend/LexCore/Entities/OrgProjects.cs new file mode 100644 index 000000000..e465fe895 --- /dev/null +++ b/backend/LexCore/Entities/OrgProjects.cs @@ -0,0 +1,9 @@ +namespace LexCore.Entities; + +public class OrgProjects : EntityBase +{ + public Guid OrgId { get; set; } + public Guid ProjectId { get; set; } + public Organization? Org { get; set; } + public Project? Project { get; set; } +} diff --git a/backend/LexCore/Entities/Organization.cs b/backend/LexCore/Entities/Organization.cs index d1d63043b..6956608a2 100644 --- a/backend/LexCore/Entities/Organization.cs +++ b/backend/LexCore/Entities/Organization.cs @@ -8,11 +8,17 @@ public class Organization : EntityBase { public required string Name { get; set; } public required List Members { get; set; } + public required List Projects { get; set; } [NotMapped] [Projectable(UseMemberBody = nameof(SqlMemberCount))] public int MemberCount { get; set; } private static Expression> SqlMemberCount => org => org.Members.Count; + + [NotMapped] + [Projectable(UseMemberBody = nameof(SqlProjectCount))] + public int ProjectCount { get; set; } + private static Expression> SqlProjectCount => org => org.Projects.Count; } public class OrgMember : EntityBase diff --git a/backend/LexCore/Entities/Project.cs b/backend/LexCore/Entities/Project.cs index 37f2e9b32..0d6114d7b 100644 --- a/backend/LexCore/Entities/Project.cs +++ b/backend/LexCore/Entities/Project.cs @@ -18,6 +18,7 @@ public class Project : EntityBase public required bool? IsConfidential { get; set; } public FlexProjectMetadata? FlexProjectMetadata { get; set; } public required List Users { get; set; } + public required List Organizations { get; set; } public required DateTimeOffset? LastCommit { get; set; } public DateTimeOffset? DeletedDate { get; set; } public ResetStatus ResetStatus { get; set; } = ResetStatus.None; diff --git a/backend/LexCore/ServiceInterfaces/IPermissionService.cs b/backend/LexCore/ServiceInterfaces/IPermissionService.cs index c23718d45..ffe8c136c 100644 --- a/backend/LexCore/ServiceInterfaces/IPermissionService.cs +++ b/backend/LexCore/ServiceInterfaces/IPermissionService.cs @@ -20,4 +20,5 @@ public interface IPermissionService void AssertCanLockOrUnlockUser(Guid userId); void AssertCanCreateOrg(); void AssertCanEditOrg(Organization org); + void AssertCanAddProjectToOrg(Organization org); } diff --git a/backend/LexData/Entities/OrgProjectsEntityConfiguration.cs b/backend/LexData/Entities/OrgProjectsEntityConfiguration.cs new file mode 100644 index 000000000..d74888a68 --- /dev/null +++ b/backend/LexData/Entities/OrgProjectsEntityConfiguration.cs @@ -0,0 +1,15 @@ +using LexCore.Entities; +using LexData.Configuration; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LexData.Entities; + +public class OrgProjectsEntityConfiguration : EntityBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + builder.HasIndex(op => new { op.OrgId, op.ProjectId }).IsUnique(); + builder.HasQueryFilter(op => op.Project!.DeletedDate == null); + } +} diff --git a/backend/LexData/Entities/OrganizationEntityConfiguration.cs b/backend/LexData/Entities/OrganizationEntityConfiguration.cs index 9ac666c8d..b8eda4636 100644 --- a/backend/LexData/Entities/OrganizationEntityConfiguration.cs +++ b/backend/LexData/Entities/OrganizationEntityConfiguration.cs @@ -16,5 +16,11 @@ public override void Configure(EntityTypeBuilder builder) .WithOne(m => m.Organization) .HasForeignKey(m => m.OrgId) .OnDelete(DeleteBehavior.Cascade); + builder.HasMany(o => o.Projects) + .WithMany(p => p.Organizations) + .UsingEntity( + op => op.HasOne(op => op.Project).WithMany().HasForeignKey(op => op.ProjectId), + op => op.HasOne(op => op.Org).WithMany().HasForeignKey(op => op.OrgId) + ); } } diff --git a/backend/LexData/Entities/ProjectEntityConfiguration.cs b/backend/LexData/Entities/ProjectEntityConfiguration.cs index add56a126..730152cb3 100644 --- a/backend/LexData/Entities/ProjectEntityConfiguration.cs +++ b/backend/LexData/Entities/ProjectEntityConfiguration.cs @@ -24,6 +24,12 @@ public override void Configure(EntityTypeBuilder builder) .WithOne(projectUser => projectUser.Project) .HasForeignKey(projectUser => projectUser.ProjectId) .OnDelete(DeleteBehavior.Cascade); + builder.HasMany(p => p.Organizations) + .WithMany(o => o.Projects) + .UsingEntity( + op => op.HasOne(op => op.Org).WithMany().HasForeignKey(op => op.OrgId), + op => op.HasOne(op => op.Project).WithMany().HasForeignKey(op => op.ProjectId) + ); builder.HasQueryFilter(p => p.DeletedDate == null); } } diff --git a/backend/LexData/LexBoxDbContext.cs b/backend/LexData/LexBoxDbContext.cs index 35ac21548..27489c257 100644 --- a/backend/LexData/LexBoxDbContext.cs +++ b/backend/LexData/LexBoxDbContext.cs @@ -29,6 +29,7 @@ protected override void ConfigureConventions(ModelConfigurationBuilder builder) public DbSet ProjectUsers => Set(); public DbSet DraftProjects => Set(); public DbSet Orgs => Set(); + public DbSet OrgProjects => Set(); public async Task HeathCheck(CancellationToken cancellationToken) { diff --git a/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.Designer.cs b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.Designer.cs new file mode 100644 index 000000000..42c80a2df --- /dev/null +++ b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.Designer.cs @@ -0,0 +1,1328 @@ +// +using System; +using System.Collections.Generic; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LexData.Migrations +{ + [DbContext(typeof(LexBoxDbContext))] + [Migration("20240606084230_AddOrgProjectsTable")] + partial class AddOrgProjectsTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", "quartz"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.ComplexProperty>("HybridDateTime", "Crdt.Core.ServerCommit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("bigint"); + + b1.Property("DateTime") + .HasColumnType("timestamp with time zone"); + }); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("CrdtCommits", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectManagerId") + .HasColumnType("uuid"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProjectManagerId"); + + b.ToTable("DraftProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("LexEntryCount") + .HasColumnType("integer"); + + b.HasKey("ProjectId"); + + b.ToTable("FlexProjectMetadata"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("UserId", "OrgId") + .IsUnique(); + + b.ToTable("OrgMembers", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("OrgId", "ProjectId") + .IsUnique(); + + b.ToTable("OrgProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Orgs", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("LastCommit") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ProjectOrigin") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ResetStatus") + .HasColumnType("integer"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId", "ProjectId") + .IsUnique(); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanCreateProjects") + .HasColumnType("boolean"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("GoogleId") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalizationCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("en"); + + b.Property("Locked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordStrength") + .HasColumnType("integer"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.HasOne("LexCore.Entities.FlexProjectMetadata", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Crdt.Core.ChangeEntity", "ChangeEntities", b1 => + { + b1.Property("ServerCommitId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Change") + .HasColumnType("text"); + + b1.Property("CommitId") + .HasColumnType("uuid"); + + b1.Property("EntityId") + .HasColumnType("uuid"); + + b1.Property("Index") + .HasColumnType("integer"); + + b1.HasKey("ServerCommitId", "Id"); + + b1.ToTable("CrdtCommits"); + + b1.ToJson("ChangeEntities"); + + b1.WithOwner() + .HasForeignKey("ServerCommitId"); + }); + + b.Navigation("ChangeEntities"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.HasOne("LexCore.Entities.User", "ProjectManager") + .WithMany() + .HasForeignKey("ProjectManagerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ProjectManager"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithOne("FlexProjectMetadata") + .HasForeignKey("LexCore.Entities.FlexProjectMetadata", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.HasOne("LexCore.Entities.Organization", "Organization") + .WithMany("Members") + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Organizations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.HasOne("LexCore.Entities.Organization", "Org") + .WithMany() + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Org"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany("Users") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.HasOne("LexCore.Entities.User", "CreatedBy") + .WithMany("UsersICreated") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Navigation("FlexProjectMetadata"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Navigation("Organizations"); + + b.Navigation("Projects"); + + b.Navigation("UsersICreated"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.cs b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.cs new file mode 100644 index 000000000..41023967b --- /dev/null +++ b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LexData.Migrations +{ + /// + public partial class AddOrgProjectsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrgProjects", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrgId = table.Column(type: "uuid", nullable: false), + ProjectId = table.Column(type: "uuid", nullable: false), + CreatedDate = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_OrgProjects", x => x.Id); + table.ForeignKey( + name: "FK_OrgProjects_Orgs_OrgId", + column: x => x.OrgId, + principalTable: "Orgs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrgProjects_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrgProjects_OrgId_ProjectId", + table: "OrgProjects", + columns: new[] { "OrgId", "ProjectId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrgProjects_OrgId", + table: "OrgProjects", + column: "OrgId"); + + migrationBuilder.CreateIndex( + name: "IX_OrgProjects_ProjectId", + table: "OrgProjects", + column: "ProjectId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrgProjects"); + } + } +} diff --git a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs index 755a1ffe0..21f188264 100644 --- a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs +++ b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs @@ -599,6 +599,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OrgMembers", (string)null); }); + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("OrgId", "ProjectId") + .IsUnique(); + + b.ToTable("OrgProjects"); + }); + modelBuilder.Entity("LexCore.Entities.Organization", b => { b.Property("Id") @@ -1150,6 +1184,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.HasOne("LexCore.Entities.Organization", "Org") + .WithMany() + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Org"); + + b.Navigation("Project"); + }); + modelBuilder.Entity("LexCore.Entities.Project", b => { b.HasOne("LexCore.Entities.Project", null) diff --git a/backend/LexData/SeedingData.cs b/backend/LexData/SeedingData.cs index e30c6368f..abea3ece3 100644 --- a/backend/LexData/SeedingData.cs +++ b/backend/LexData/SeedingData.cs @@ -22,6 +22,9 @@ public class SeedingData( public static readonly Guid QaAdminId = new("99b00c58-0dc7-4fe4-b6f2-c27b828811e0"); private static readonly Guid MangerId = new Guid("703701a8-005c-4747-91f2-ac7650455118"); private static readonly Guid EditorId = new Guid("6dc9965b-4021-4606-92df-133fcce75fcb"); + private static readonly Guid TestOrgId = new Guid("292c80e6-a815-4cd1-9ea2-34bd01274de6"); + private static readonly Guid SecondTestOrgId = new Guid("a748bd8b-6348-4980-8dee-6de8b63e4a39"); + private static readonly Guid Sena3ProjId = new Guid("0ebc5976-058d-4447-aaa7-297f8569f968"); public async Task SeedIfNoUsers(CancellationToken cancellationToken = default) { @@ -93,7 +96,7 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) lexBoxDbContext.Attach(new Project { - Id = new Guid("0ebc5976-058d-4447-aaa7-297f8569f968"), + Id = Sena3ProjId, Name = "Sena 3", Code = "sena-3", Type = ProjectType.FLEx, @@ -105,6 +108,7 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) LexEntryCount = -1 }, IsConfidential = null, + Organizations = [], Users = new() { new() @@ -154,13 +158,15 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) LastCommit = DateTimeOffset.UtcNow, RetentionPolicy = RetentionPolicy.Dev, IsConfidential = false, + Organizations = [], Users = [], }); lexBoxDbContext.Attach(new Organization { - Id = new Guid("292c80e6-a815-4cd1-9ea2-34bd01274de6"), + Id = TestOrgId, Name = "Test Org", + Projects = [], Members = [ new OrgMember @@ -174,6 +180,35 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) ] }); + lexBoxDbContext.Attach(new Organization + { + Id = SecondTestOrgId, + Name = "Second Test Org", + Projects = [], + Members = + [ + new OrgMember + { + Id = new Guid("03d54e43-ba53-410f-adc2-5ae0bc3cfb21"), Role = OrgRole.Admin, UserId = MangerId, + }, + new OrgMember + { + Id = new Guid("d00c7149-c3b2-448a-93ed-9ba2746d38f0"), Role = OrgRole.User, UserId = EditorId, + }, + new OrgMember + { + Id = new Guid("3035a412-8503-465b-8525-b60aaadd9488"), Role = OrgRole.User, UserId = TestAdminId, + }, + ] + }); + + lexBoxDbContext.Attach(new OrgProjects + { + Id = new Guid("f659eb4c-0289-475d-b44a-095ffddb31c8"), + OrgId = TestOrgId, + ProjectId = Sena3ProjId, + }); + foreach (var entry in lexBoxDbContext.ChangeTracker.Entries()) { var exists = await entry.GetDatabaseValuesAsync(cancellationToken) is not null; diff --git a/backend/Testing/LexCore/Services/ProjectServiceTest.cs b/backend/Testing/LexCore/Services/ProjectServiceTest.cs index f4b701628..49e785c49 100644 --- a/backend/Testing/LexCore/Services/ProjectServiceTest.cs +++ b/backend/Testing/LexCore/Services/ProjectServiceTest.cs @@ -33,7 +33,7 @@ public ProjectServiceTest(TestingServicesFixture testing) public async Task CanCreateProject() { var projectId = await _projectService.CreateProject( - new(null, "TestProject", "Test", "test", ProjectType.FLEx, RetentionPolicy.Test, false, null)); + new(null, "TestProject", "Test", "test", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); projectId.ShouldNotBe(default); } @@ -42,10 +42,10 @@ public async Task ShouldErrorIfCreatingAProjectWithTheSameCode() { //first project should be created await _projectService.CreateProject( - new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, false, null)); + new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); var exception = await _projectService.CreateProject( - new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, false, null) + new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, false, null, null) ).ShouldThrowAsync(); exception.InnerException.ShouldBeOfType() diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 777982f51..ba45f077b 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -12,6 +12,11 @@ type AddProjectMemberPayload { errors: [AddProjectMemberError!] } +type AddProjectToOrgPayload { + organization: Organization + errors: [AddProjectToOrgError!] +} + type AlreadyExistsError implements Error { message: String! } @@ -192,6 +197,8 @@ type MeDto { type Mutation { createOrganization(input: CreateOrganizationInput!): CreateOrganizationPayload! deleteOrg(input: DeleteOrgInput!): DeleteOrgPayload! @authorize(policy: "AdminRequiredPolicy") + addProjectToOrg(input: AddProjectToOrgInput!): AddProjectToOrgPayload! + removeProjectFromOrg(input: RemoveProjectFromOrgInput!): RemoveProjectFromOrgPayload! setOrgMemberRole(input: SetOrgMemberRoleInput!): SetOrgMemberRolePayload! changeOrgMemberRole(input: ChangeOrgMemberRoleInput!): ChangeOrgMemberRolePayload! changeOrgName(input: ChangeOrgNameInput!): ChangeOrgNamePayload! @@ -229,11 +236,23 @@ type OrgMember { updatedDate: DateTime! } +type OrgProjects { + org: Organization! + project: Project! + orgId: UUID! + projectId: UUID! + id: UUID! + createdDate: DateTime! + updatedDate: DateTime! +} + type Organization { createdDate: DateTime! - memberCount: Int! name: String! members: [OrgMember!]! + projects: [Project!]! + memberCount: Int! + projectCount: Int! id: UUID! updatedDate: DateTime! } @@ -253,6 +272,7 @@ type Project { type: ProjectType! isConfidential: Boolean flexProjectMetadata: FlexProjectMetadata + organizations: [Organization!]! lastCommit: DateTime deletedDate: DateTime resetStatus: ResetStatus! @@ -291,8 +311,8 @@ type ProjectUsers { type Query { myProjects(orderBy: [ProjectSortInput!]): [Project!]! - myDraftProjects(orderBy: [DraftProjectSortInput!]): [DraftProject!]! projects(withDeleted: Boolean! = false where: ProjectFilterInput orderBy: [ProjectSortInput!]): [Project!]! @authorize(policy: "AdminRequiredPolicy") + myDraftProjects(orderBy: [DraftProjectSortInput!]): [DraftProject!]! draftProjects(where: DraftProjectFilterInput orderBy: [DraftProjectSortInput!]): [DraftProject!]! @authorize(policy: "AdminRequiredPolicy") projectById(projectId: UUID!): Project projectByCode(code: String!): Project @@ -307,6 +327,11 @@ type Query { isAdmin: IsAdminResponse! @authorize(policy: "AdminRequiredPolicy") } +type RemoveProjectFromOrgPayload { + organization: Organization + errors: [RemoveProjectFromOrgError!] +} + type RemoveProjectMemberPayload { project: Project } @@ -377,6 +402,8 @@ type UsersCollectionSegment { union AddProjectMemberError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole | ProjectMemberInvitedByEmail | InvalidEmailError | AlreadyExistsError +union AddProjectToOrgError = DbError | NotFoundError + union BulkAddProjectMembersError = NotFoundError | InvalidEmailError | DbError union ChangeOrgMemberRoleError = DbError | NotFoundError @@ -407,6 +434,8 @@ union DeleteUserByAdminOrSelfError = NotFoundError | DbError union LeaveProjectError = NotFoundError | LastMemberCantLeaveError +union RemoveProjectFromOrgError = DbError | NotFoundError + union SetOrgMemberRoleError = DbError | NotFoundError union SetProjectConfidentialityError = NotFoundError | DbError @@ -421,6 +450,11 @@ input AddProjectMemberInput { role: ProjectRole! } +input AddProjectToOrgInput { + orgId: UUID! + projectId: UUID! +} + input BooleanOperationFilterInput { eq: Boolean neq: Boolean @@ -495,6 +529,7 @@ input CreateProjectInput { type: ProjectType! retentionPolicy: RetentionPolicy! isConfidential: Boolean! + owningOrgId: UUID projectManagerId: UUID } @@ -593,6 +628,20 @@ input ListFilterInputTypeOfOrgMemberFilterInput { any: Boolean } +input ListFilterInputTypeOfOrganizationFilterInput { + all: OrganizationFilterInput + none: OrganizationFilterInput + some: OrganizationFilterInput + any: Boolean +} + +input ListFilterInputTypeOfProjectFilterInput { + all: ProjectFilterInput + none: ProjectFilterInput + some: ProjectFilterInput + any: Boolean +} + input ListFilterInputTypeOfProjectUsersFilterInput { all: ProjectUsersFilterInput none: ProjectUsersFilterInput @@ -632,7 +681,9 @@ input OrganizationFilterInput { or: [OrganizationFilterInput!] name: StringOperationFilterInput members: ListFilterInputTypeOfOrgMemberFilterInput + projects: ListFilterInputTypeOfProjectFilterInput memberCount: IntOperationFilterInput + projectCount: IntOperationFilterInput id: UuidOperationFilterInput createdDate: DateTimeOperationFilterInput updatedDate: DateTimeOperationFilterInput @@ -641,6 +692,7 @@ input OrganizationFilterInput { input OrganizationSortInput { name: SortEnumType memberCount: SortEnumType + projectCount: SortEnumType id: SortEnumType createdDate: SortEnumType updatedDate: SortEnumType @@ -658,6 +710,7 @@ input ProjectFilterInput { isConfidential: BooleanOperationFilterInput flexProjectMetadata: FlexProjectMetadataFilterInput users: ListFilterInputTypeOfProjectUsersFilterInput + organizations: ListFilterInputTypeOfOrganizationFilterInput lastCommit: DateTimeOperationFilterInput deletedDate: DateTimeOperationFilterInput resetStatus: ResetStatusOperationFilterInput @@ -723,6 +776,11 @@ input ProjectUsersFilterInput { updatedDate: DateTimeOperationFilterInput } +input RemoveProjectFromOrgInput { + orgId: UUID! + projectId: UUID! +} + input RemoveProjectMemberInput { projectId: UUID! userId: UUID! diff --git a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte index f4b7c35c0..6e9b21f11 100644 --- a/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte +++ b/frontend/src/routes/(authenticated)/org/[org_id]/+page.svelte @@ -19,6 +19,7 @@ import ChangeOrgMemberRoleModal from './ChangeOrgMemberRoleModal.svelte'; import UserModal from '$lib/components/Users/UserModal.svelte'; import OrgMemberTable from './OrgMemberTable.svelte'; + import ProjectTable from '$lib/components/Projects/ProjectTable.svelte'; export let data: PageData; $: user = data.user; @@ -112,12 +113,14 @@
- - +
{#if $queryParamValues.tab === 'projects'} - Projects list will go here once orgs have projects associated with them + {:else if $queryParamValues.tab === 'members'}