Adding Flags API

This commit is contained in:
2026-04-07 15:30:40 -07:00
parent 7b15086fe5
commit 0ba076b650
78 changed files with 3862 additions and 81 deletions

View File

@@ -0,0 +1,47 @@
using MicCheck.Api.Authorization;
using MicCheck.Api.Features;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MicCheck.Api.Identities;
[ApiController]
[Route("api/v1/identities")]
[Authorize(Policy = AuthorizationPolicies.FlagsApiAccess)]
public class IdentitiesController(
FeatureEvaluationService featureEvaluationService,
IdentityResolutionService identityResolutionService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IdentityResponse>> GetByIdentifier(
[FromQuery] string identifier, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(identifier))
return BadRequest("identifier query parameter is required.");
var environmentId = int.Parse(User.FindFirst("EnvironmentId")!.Value!);
var identity = await identityResolutionService.ResolveAsync(environmentId, identifier, null, ct);
var flags = await featureEvaluationService.EvaluateForIdentityAsync(environmentId, identifier, null, ct);
return Ok(new IdentityResponse(
identity.Traits.Select(TraitResponse.From).ToList(),
flags.Select(FlagResponse.From).ToList()));
}
[HttpPost]
public async Task<ActionResult<IdentityResponse>> Identify(
[FromBody] IdentityRequest request, CancellationToken ct)
{
var environmentId = int.Parse(User.FindFirst("EnvironmentId")!.Value!);
var identity = await identityResolutionService.ResolveAsync(
environmentId, request.Identifier, request.Traits, ct);
var flags = await featureEvaluationService.EvaluateForIdentityAsync(
environmentId, request.Identifier, request.Traits, ct);
return Ok(new IdentityResponse(
identity.Traits.Select(TraitResponse.From).ToList(),
flags.Select(FlagResponse.From).ToList()));
}
}

View File

@@ -0,0 +1,3 @@
namespace MicCheck.Api.Identities;
public record IdentityRequest(string Identifier, IReadOnlyList<TraitInput>? Traits);

View File

@@ -0,0 +1,58 @@
using MicCheck.Api.Data;
using Microsoft.EntityFrameworkCore;
namespace MicCheck.Api.Identities;
public class IdentityResolutionService(MicCheckDbContext db)
{
public async Task<Identity> ResolveAsync(
int environmentId,
string identifier,
IReadOnlyList<TraitInput>? traits,
CancellationToken ct = default)
{
var identity = await db.Identities
.Include(i => i.Traits)
.FirstOrDefaultAsync(i => i.EnvironmentId == environmentId && i.Identifier == identifier, ct);
if (identity is null)
{
identity = new Identity
{
Identifier = identifier,
EnvironmentId = environmentId,
CreatedAt = DateTimeOffset.UtcNow
};
db.Identities.Add(identity);
await db.SaveChangesAsync(ct);
}
if (traits is { Count: > 0 })
{
foreach (var traitInput in traits)
{
var existing = identity.Traits.FirstOrDefault(t => t.Key == traitInput.TraitKey);
if (existing is not null)
{
existing.Value = traitInput.GetStringValue();
existing.ValueType = traitInput.GetValueType();
}
else
{
var newTrait = new IdentityTrait
{
IdentityId = identity.Id,
Key = traitInput.TraitKey,
Value = traitInput.GetStringValue(),
ValueType = traitInput.GetValueType()
};
db.IdentityTraits.Add(newTrait);
identity.Traits.Add(newTrait);
}
}
await db.SaveChangesAsync(ct);
}
return identity;
}
}

View File

@@ -0,0 +1,7 @@
using MicCheck.Api.Features;
namespace MicCheck.Api.Identities;
public record IdentityResponse(
IReadOnlyList<TraitResponse> Traits,
IReadOnlyList<FlagResponse> Flags);

View File

@@ -0,0 +1,31 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MicCheck.Api.Identities;
public record TraitInput(
[property: JsonPropertyName("trait_key")] string TraitKey,
[property: JsonPropertyName("trait_value")] object? TraitValue)
{
public string GetStringValue() => TraitValue switch
{
JsonElement el when el.ValueKind == JsonValueKind.String => el.GetString() ?? string.Empty,
JsonElement el when el.ValueKind == JsonValueKind.True => "true",
JsonElement el when el.ValueKind == JsonValueKind.False => "false",
JsonElement el when el.ValueKind == JsonValueKind.Null => string.Empty,
JsonElement el => el.ToString(),
null => string.Empty,
_ => TraitValue.ToString() ?? string.Empty
};
public TraitValueType GetValueType() => TraitValue switch
{
JsonElement el when el.ValueKind is JsonValueKind.True or JsonValueKind.False => TraitValueType.Boolean,
JsonElement el when el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out _) => TraitValueType.Integer,
JsonElement el when el.ValueKind == JsonValueKind.Number => TraitValueType.Float,
bool => TraitValueType.Boolean,
int or long => TraitValueType.Integer,
float or double or decimal => TraitValueType.Float,
_ => TraitValueType.String
};
}

View File

@@ -0,0 +1,7 @@
namespace MicCheck.Api.Identities;
public record TraitResponse(string TraitKey, string TraitValue)
{
public static TraitResponse From(IdentityTrait trait) =>
new(trait.Key, trait.Value);
}