UX tweaks, traits bug fix, adding member info
This commit is contained in:
@@ -18,7 +18,7 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
builder.HasIndex(u => u.Email).IsUnique();
|
||||
|
||||
builder.HasMany(u => u.Organizations)
|
||||
.WithOne()
|
||||
.WithOne(ou => ou.User)
|
||||
.HasForeignKey(ou => ou.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/api/MicCheck.Api/MicCheck.Api.csproj", "src/api/MicCheck.Api/"]
|
||||
RUN dotnet restore "src/api/MicCheck.Api/MicCheck.Api.csproj"
|
||||
COPY . .
|
||||
RUN dotnet build "src/api/MicCheck.Api/MicCheck.Api.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "src/api/MicCheck.Api/MicCheck.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MicCheck.Api.dll"]
|
||||
|
||||
@@ -163,6 +163,24 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return Ok(new PaginatedResponse<Identities.AdminIdentityResponse>(total, null, null, results));
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}/identities")]
|
||||
public async Task<ActionResult<Identities.AdminIdentityResponse>> CreateIdentity(
|
||||
string apiKey,
|
||||
[FromBody] Identities.CreateIdentityRequest request,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var identity = await adminIdentityService.CreateAsync(environment.Id, request.Identifier, ct);
|
||||
if (identity is null)
|
||||
return Conflict(new { error = "An identity with this identifier already exists in the environment." });
|
||||
|
||||
return CreatedAtAction(nameof(GetIdentity), new { apiKey, id = identity.Id },
|
||||
Identities.AdminIdentityResponse.From(identity));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/identities/{id}")]
|
||||
public async Task<ActionResult<Identities.AdminIdentityResponse>> GetIdentity(
|
||||
string apiKey, int id,
|
||||
@@ -193,6 +211,35 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/identities/{id}/traits/{key}")]
|
||||
public async Task<ActionResult<Identities.TraitResponse>> UpsertIdentityTrait(
|
||||
string apiKey, int id, string key,
|
||||
[FromBody] Identities.UpsertTraitRequest request,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var result = await adminIdentityService.UpsertTraitAsync(id, environment.Id, key, request.Value, ct);
|
||||
if (result is null) return NotFound();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/identities/{id}/traits/{key}")]
|
||||
public async Task<IActionResult> DeleteIdentityTrait(
|
||||
string apiKey, int id, string key,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var deleted = await adminIdentityService.DeleteTraitAsync(id, environment.Id, key, ct);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/identities/{id}/featurestates")]
|
||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> GetIdentityFeatureStates(
|
||||
string apiKey, int id,
|
||||
@@ -302,4 +349,95 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
||||
var updated = await featureStateService.PatchAsync(id, request.Enabled, request.Value, ct);
|
||||
return Ok(Features.FeatureStateResponse.From(updated));
|
||||
}
|
||||
|
||||
// ─── Feature Segments ────────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{apiKey}/features/{featureId}/segments")]
|
||||
public async Task<ActionResult<IReadOnlyList<Features.FeatureSegmentResponse>>> ListFeatureSegments(
|
||||
string apiKey, int featureId,
|
||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var results = await featureSegmentService.ListByFeatureAsync(featureId, environment.Id, ct);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}/features/{featureId}/segments")]
|
||||
public async Task<ActionResult<Features.FeatureSegmentResponse>> CreateFeatureSegment(
|
||||
string apiKey, int featureId,
|
||||
Features.CreateFeatureSegmentRequest request,
|
||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await featureSegmentService.CreateAsync(
|
||||
featureId, environment.Id, request.SegmentId, request.Priority,
|
||||
request.Enabled, request.Value, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{apiKey}/features/{featureId}/segments/{id}")]
|
||||
public async Task<ActionResult<Features.FeatureSegmentResponse>> UpdateFeatureSegment(
|
||||
string apiKey, int featureId, int id,
|
||||
Features.UpdateFeatureSegmentRequest request,
|
||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var result = await featureSegmentService.UpdateAsync(id, request.Priority, request.Enabled, request.Value, ct);
|
||||
if (result is null) return NotFound();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("{apiKey}/features/{featureId}/segments/{id}")]
|
||||
public async Task<IActionResult> DeleteFeatureSegment(
|
||||
string apiKey, int featureId, int id,
|
||||
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var deleted = await featureSegmentService.DeleteAsync(id, ct);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
// ─── Identity Segments ───────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{apiKey}/identities/{id}/segments")]
|
||||
public async Task<ActionResult<IReadOnlyList<Segments.SegmentSummaryResponse>>> GetIdentitySegments(
|
||||
string apiKey, int id,
|
||||
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||
[FromServices] Segments.SegmentService segmentService,
|
||||
[FromServices] Segments.SegmentEvaluator segmentEvaluator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||
if (environment is null) return NotFound();
|
||||
|
||||
var identity = await adminIdentityService.FindByIdAsync(id, environment.Id, ct);
|
||||
if (identity is null) return NotFound();
|
||||
|
||||
var segments = await segmentService.ListByProjectAsync(environment.ProjectId, ct);
|
||||
var matching = segments
|
||||
.Where(s => segmentEvaluator.Evaluate(s, identity.Traits.ToList(), identity.Identifier))
|
||||
.Select(s => new Segments.SegmentSummaryResponse(s.Id, s.Name))
|
||||
.ToList();
|
||||
|
||||
return Ok(matching);
|
||||
}
|
||||
}
|
||||
|
||||
5
src/api/MicCheck.Api/Features/FeatureSegmentRequest.cs
Normal file
5
src/api/MicCheck.Api/Features/FeatureSegmentRequest.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public record CreateFeatureSegmentRequest(int SegmentId, int Priority, bool Enabled, string? Value);
|
||||
|
||||
public record UpdateFeatureSegmentRequest(int Priority, bool Enabled, string? Value);
|
||||
12
src/api/MicCheck.Api/Features/FeatureSegmentResponse.cs
Normal file
12
src/api/MicCheck.Api/Features/FeatureSegmentResponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public record FeatureSegmentResponse(
|
||||
int Id,
|
||||
int FeatureId,
|
||||
int SegmentId,
|
||||
string SegmentName,
|
||||
int EnvironmentId,
|
||||
int Priority,
|
||||
bool? Enabled,
|
||||
string? Value
|
||||
);
|
||||
124
src/api/MicCheck.Api/Features/FeatureSegmentService.cs
Normal file
124
src/api/MicCheck.Api/Features/FeatureSegmentService.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Segments;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureSegmentService(MicCheckDbContext db)
|
||||
{
|
||||
public async Task<IReadOnlyList<FeatureSegmentResponse>> ListByFeatureAsync(
|
||||
int featureId, int environmentId, CancellationToken ct = default)
|
||||
{
|
||||
var featureSegments = await db.FeatureSegments
|
||||
.Where(fsg => fsg.FeatureId == featureId && fsg.EnvironmentId == environmentId)
|
||||
.OrderBy(fsg => fsg.Priority)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var segmentIds = featureSegments.Select(fsg => fsg.SegmentId).Distinct().ToList();
|
||||
var segments = await db.Segments
|
||||
.Where(s => segmentIds.Contains(s.Id))
|
||||
.ToDictionaryAsync(s => s.Id, ct);
|
||||
|
||||
var states = await db.FeatureStates
|
||||
.Where(fs => fs.FeatureId == featureId && fs.EnvironmentId == environmentId && fs.FeatureSegmentId != null)
|
||||
.ToListAsync(ct);
|
||||
var stateBySegment = states.ToDictionary(fs => fs.FeatureSegmentId!.Value);
|
||||
|
||||
return featureSegments.Select(fsg =>
|
||||
{
|
||||
var segmentName = segments.TryGetValue(fsg.SegmentId, out var seg) ? seg.Name : "Unknown";
|
||||
stateBySegment.TryGetValue(fsg.Id, out var state);
|
||||
return new FeatureSegmentResponse(
|
||||
fsg.Id, fsg.FeatureId, fsg.SegmentId, segmentName,
|
||||
fsg.EnvironmentId, fsg.Priority,
|
||||
state?.Enabled, state?.Value);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<FeatureSegmentResponse> CreateAsync(
|
||||
int featureId, int environmentId, int segmentId, int priority, bool enabled, string? value,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var segment = await db.Segments.FirstOrDefaultAsync(s => s.Id == segmentId, ct)
|
||||
?? throw new KeyNotFoundException($"Segment {segmentId} not found.");
|
||||
|
||||
var featureSegment = new FeatureSegment
|
||||
{
|
||||
FeatureId = featureId,
|
||||
SegmentId = segmentId,
|
||||
EnvironmentId = environmentId,
|
||||
Priority = priority
|
||||
};
|
||||
db.FeatureSegments.Add(featureSegment);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
var state = new FeatureState
|
||||
{
|
||||
FeatureId = featureId,
|
||||
EnvironmentId = environmentId,
|
||||
FeatureSegmentId = featureSegment.Id,
|
||||
Enabled = enabled,
|
||||
Value = value,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
db.FeatureStates.Add(state);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return new FeatureSegmentResponse(
|
||||
featureSegment.Id, featureId, segmentId, segment.Name,
|
||||
environmentId, priority, enabled, value);
|
||||
}
|
||||
|
||||
public async Task<FeatureSegmentResponse?> UpdateAsync(
|
||||
int id, int priority, bool enabled, string? value, CancellationToken ct = default)
|
||||
{
|
||||
var featureSegment = await db.FeatureSegments.FirstOrDefaultAsync(fsg => fsg.Id == id, ct);
|
||||
if (featureSegment is null) return null;
|
||||
|
||||
var segment = await db.Segments.FirstOrDefaultAsync(s => s.Id == featureSegment.SegmentId, ct);
|
||||
|
||||
featureSegment.Priority = priority;
|
||||
|
||||
var state = await db.FeatureStates
|
||||
.FirstOrDefaultAsync(fs => fs.FeatureSegmentId == id, ct);
|
||||
|
||||
if (state is not null)
|
||||
{
|
||||
state.Enabled = enabled;
|
||||
state.Value = value;
|
||||
state.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = new FeatureState
|
||||
{
|
||||
FeatureId = featureSegment.FeatureId,
|
||||
EnvironmentId = featureSegment.EnvironmentId,
|
||||
FeatureSegmentId = featureSegment.Id,
|
||||
Enabled = enabled,
|
||||
Value = value,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
db.FeatureStates.Add(state);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return new FeatureSegmentResponse(
|
||||
featureSegment.Id, featureSegment.FeatureId, featureSegment.SegmentId,
|
||||
segment?.Name ?? "Unknown", featureSegment.EnvironmentId,
|
||||
priority, enabled, value);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var featureSegment = await db.FeatureSegments.FirstOrDefaultAsync(fsg => fsg.Id == id, ct);
|
||||
if (featureSegment is null) return false;
|
||||
|
||||
db.FeatureSegments.Remove(featureSegment);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,25 @@ public class AdminIdentityService(MicCheckDbContext db)
|
||||
return (total, items);
|
||||
}
|
||||
|
||||
public async Task<Identity?> CreateAsync(int environmentId, string identifier, CancellationToken ct = default)
|
||||
{
|
||||
var existing = await db.Identities
|
||||
.FirstOrDefaultAsync(i => i.EnvironmentId == environmentId && i.Identifier == identifier, ct);
|
||||
|
||||
if (existing is not null)
|
||||
return null;
|
||||
|
||||
var identity = new Identity
|
||||
{
|
||||
Identifier = identifier,
|
||||
EnvironmentId = environmentId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
db.Identities.Add(identity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async Task<Identity?> FindByIdAsync(int id, int environmentId, CancellationToken ct = default)
|
||||
{
|
||||
return await db.Identities
|
||||
@@ -30,6 +49,53 @@ public class AdminIdentityService(MicCheckDbContext db)
|
||||
.FirstOrDefaultAsync(i => i.Id == id && i.EnvironmentId == environmentId, ct);
|
||||
}
|
||||
|
||||
public async Task<TraitResponse?> UpsertTraitAsync(
|
||||
int identityId, int environmentId, string key, string value, CancellationToken ct = default)
|
||||
{
|
||||
var identity = await db.Identities
|
||||
.Include(i => i.Traits)
|
||||
.FirstOrDefaultAsync(i => i.Id == identityId && i.EnvironmentId == environmentId, ct);
|
||||
|
||||
if (identity is null) return null;
|
||||
|
||||
var existing = identity.Traits.FirstOrDefault(t => t.Key == key);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var trait = new IdentityTrait
|
||||
{
|
||||
IdentityId = identityId,
|
||||
Key = key,
|
||||
Value = value,
|
||||
ValueType = TraitValueType.String,
|
||||
};
|
||||
db.IdentityTraits.Add(trait);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new TraitResponse(key, value);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteTraitAsync(
|
||||
int identityId, int environmentId, string key, CancellationToken ct = default)
|
||||
{
|
||||
var identity = await db.Identities
|
||||
.Include(i => i.Traits)
|
||||
.FirstOrDefaultAsync(i => i.Id == identityId && i.EnvironmentId == environmentId, ct);
|
||||
|
||||
if (identity is null) return false;
|
||||
|
||||
var trait = identity.Traits.FirstOrDefault(t => t.Key == key);
|
||||
if (trait is null) return false;
|
||||
|
||||
db.IdentityTraits.Remove(trait);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var identity = await db.Identities.FirstOrDefaultAsync(i => i.Id == id, ct);
|
||||
|
||||
3
src/api/MicCheck.Api/Identities/CreateIdentityRequest.cs
Normal file
3
src/api/MicCheck.Api/Identities/CreateIdentityRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MicCheck.Api.Identities;
|
||||
|
||||
public record CreateIdentityRequest(string Identifier);
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MicCheck.Api.Identities;
|
||||
|
||||
public record TraitResponse(string TraitKey, string TraitValue)
|
||||
public record TraitResponse(string Key, string Value)
|
||||
{
|
||||
public static TraitResponse From(IdentityTrait trait) =>
|
||||
new(trait.Key, trait.Value);
|
||||
|
||||
3
src/api/MicCheck.Api/Identities/UpsertTraitRequest.cs
Normal file
3
src/api/MicCheck.Api/Identities/UpsertTraitRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MicCheck.Api.Identities;
|
||||
|
||||
public record UpsertTraitRequest(string Key, string Value);
|
||||
928
src/api/MicCheck.Api/Migrations/20260416184635_AddOrganizationUserNavigation.Designer.cs
generated
Normal file
928
src/api/MicCheck.Api/Migrations/20260416184635_AddOrganizationUserNavigation.Designer.cs
generated
Normal file
@@ -0,0 +1,928 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MicCheck.Api.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 MicCheck.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(MicCheckDbContext))]
|
||||
[Migration("20260416184635_AddOrganizationUserNavigation")]
|
||||
partial class AddOrganizationUserNavigation
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FeatureTags", b =>
|
||||
{
|
||||
b.Property<int>("FeatureId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TagsId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("FeatureId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("FeatureTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.ApiKeys.ApiKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Prefix")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Audit.AuditLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int?>("ActorUserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Changes")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ResourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId", "CreatedAt");
|
||||
|
||||
b.ToTable("AuditLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Authorization.UserProjectPermission", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "ProjectId");
|
||||
|
||||
b.ToTable("UserProjectPermissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Environments.Environment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApiKey")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Environments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Feature", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("DefaultEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("InitialValue")
|
||||
.HasMaxLength(20000)
|
||||
.HasColumnType("character varying(20000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Features");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureSegment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("FeatureId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SegmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("FeatureSegments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureState", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("FeatureId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FeatureSegmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("IdentityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(20000)
|
||||
.HasColumnType("character varying(20000)");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EnvironmentId");
|
||||
|
||||
b.HasIndex("FeatureSegmentId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("IdentityId");
|
||||
|
||||
b.HasIndex("FeatureId", "EnvironmentId", "IdentityId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FeatureStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Color")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.Identity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EnvironmentId", "Identifier")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Identities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.IdentityTrait", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("IdentityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("ValueType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IdentityId", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("IdentityTraits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.Organization", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Organizations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.OrganizationUser", b =>
|
||||
{
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("OrganizationId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("OrganizationUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("HideDisabledFlags")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.Segment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProjectId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("Segments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentCondition", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Operator")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Property")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("RuleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RuleId");
|
||||
|
||||
b.ToTable("SegmentConditions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentRule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("ParentRuleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SegmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRuleId");
|
||||
|
||||
b.HasIndex("SegmentId");
|
||||
|
||||
b.ToTable("SegmentRules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.RefreshToken", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsRevoked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsTwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastLoginAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Webhooks.Webhook", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int?>("EnvironmentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("OrganizationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Scope")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Secret")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EnvironmentId");
|
||||
|
||||
b.ToTable("Webhooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Webhooks.WebhookDeliveryLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AttemptNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("AttemptedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeSpan>("Duration")
|
||||
.HasColumnType("interval");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("PayloadJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ResponseBody")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("ResponseStatusCode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("Success")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("WebhookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WebhookId");
|
||||
|
||||
b.ToTable("WebhookDeliveryLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FeatureTags", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Features.Feature", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("FeatureId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Features.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.ApiKeys.ApiKey", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Organizations.Organization", null)
|
||||
.WithMany("ApiKeys")
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Environments.Environment", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany("Environments")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Feature", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany("Features")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureState", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Environments.Environment", null)
|
||||
.WithMany("FeatureStates")
|
||||
.HasForeignKey("EnvironmentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Features.Feature", null)
|
||||
.WithMany("FeatureStates")
|
||||
.HasForeignKey("FeatureId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Features.FeatureSegment", null)
|
||||
.WithOne("FeatureState")
|
||||
.HasForeignKey("MicCheck.Api.Features.FeatureState", "FeatureSegmentId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("MicCheck.Api.Identities.Identity", null)
|
||||
.WithMany("FeatureStateOverrides")
|
||||
.HasForeignKey("IdentityId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Tag", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.IdentityTrait", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Identities.Identity", null)
|
||||
.WithMany("Traits")
|
||||
.HasForeignKey("IdentityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.OrganizationUser", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Organizations.Organization", null)
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Users.User", "User")
|
||||
.WithMany("Organizations")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Organizations.Organization", null)
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("OrganizationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.Segment", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Projects.Project", null)
|
||||
.WithMany("Segments")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentCondition", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Segments.SegmentRule", null)
|
||||
.WithMany("Conditions")
|
||||
.HasForeignKey("RuleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentRule", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Segments.SegmentRule", null)
|
||||
.WithMany("ChildRules")
|
||||
.HasForeignKey("ParentRuleId")
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
|
||||
b.HasOne("MicCheck.Api.Segments.Segment", null)
|
||||
.WithMany("Rules")
|
||||
.HasForeignKey("SegmentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("MicCheck.Api.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Environments.Environment", b =>
|
||||
{
|
||||
b.Navigation("FeatureStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.Feature", b =>
|
||||
{
|
||||
b.Navigation("FeatureStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Features.FeatureSegment", b =>
|
||||
{
|
||||
b.Navigation("FeatureState");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Identities.Identity", b =>
|
||||
{
|
||||
b.Navigation("FeatureStateOverrides");
|
||||
|
||||
b.Navigation("Traits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Organizations.Organization", b =>
|
||||
{
|
||||
b.Navigation("ApiKeys");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Environments");
|
||||
|
||||
b.Navigation("Features");
|
||||
|
||||
b.Navigation("Segments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.Segment", b =>
|
||||
{
|
||||
b.Navigation("Rules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Segments.SegmentRule", b =>
|
||||
{
|
||||
b.Navigation("ChildRules");
|
||||
|
||||
b.Navigation("Conditions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Users.User", b =>
|
||||
{
|
||||
b.Navigation("Organizations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MicCheck.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOrganizationUserNavigation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -802,11 +802,13 @@ namespace MicCheck.Api.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("MicCheck.Api.Users.User", null)
|
||||
b.HasOne("MicCheck.Api.Users.User", "User")
|
||||
.WithMany("Organizations")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MicCheck.Api.Projects.Project", b =>
|
||||
|
||||
@@ -12,9 +12,18 @@ public record OrganizationResponse(
|
||||
|
||||
public record OrganizationMemberResponse(
|
||||
int UserId,
|
||||
string Role
|
||||
string FirstName,
|
||||
string LastName,
|
||||
string Email,
|
||||
string Role,
|
||||
DateTimeOffset? LastLoginAt
|
||||
)
|
||||
{
|
||||
public static OrganizationMemberResponse From(OrganizationUser member) => new(
|
||||
member.UserId, member.Role.ToString());
|
||||
member.UserId,
|
||||
member.User.FirstName,
|
||||
member.User.LastName,
|
||||
member.User.Email,
|
||||
member.Role.ToString(),
|
||||
member.User.LastLoginAt);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ public class OrganizationService(MicCheckDbContext db, AuditService auditService
|
||||
{
|
||||
return await db.OrganizationUsers
|
||||
.Where(m => m.OrganizationId == organizationId)
|
||||
.Include(m => m.User)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MicCheck.Api.Users;
|
||||
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
public class OrganizationUser
|
||||
@@ -5,6 +7,7 @@ public class OrganizationUser
|
||||
public int OrganizationId { get; init; }
|
||||
public int UserId { get; init; }
|
||||
public OrganizationRole Role { get; set; }
|
||||
public User User { get; init; } = null!;
|
||||
}
|
||||
|
||||
public enum OrganizationRole { User, Admin }
|
||||
|
||||
@@ -126,6 +126,7 @@ try
|
||||
builder.Services.AddScoped<EnvironmentService>();
|
||||
builder.Services.AddScoped<FeatureService>();
|
||||
builder.Services.AddScoped<FeatureStateService>();
|
||||
builder.Services.AddScoped<FeatureSegmentService>();
|
||||
builder.Services.AddScoped<SegmentService>();
|
||||
builder.Services.AddScoped<TagService>();
|
||||
builder.Services.AddScoped<WebhookService>();
|
||||
|
||||
3
src/api/MicCheck.Api/Segments/SegmentSummaryResponse.cs
Normal file
3
src/api/MicCheck.Api/Segments/SegmentSummaryResponse.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MicCheck.Api.Segments;
|
||||
|
||||
public record SegmentSummaryResponse(int Id, string Name);
|
||||
Reference in New Issue
Block a user