using FozFc.Api.Api.Authorization; using FozFc.Api.Api.Extensions; using FozFc.Api.Application.Common; using FozFc.Api.Application.Queries; using FozFc.Api.Domain; using FozFc.Api.Infrastructure.Mongo; using MongoDB.Bson; namespace FozFc.Api.Api.Endpoints; /// /// Generic REST CRUD for every known collection (the Firestore replacement). /// Literal auth/upload/contact routes take precedence over these parameterized routes. /// public static class CrudEndpoints { public static IEndpointRouteBuilder MapCrudEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api"); group.MapGet("/{collection}", async (string collection, HttpContext ctx, IDocumentRepository repo, CancellationToken ct) => { EnsureKnown(collection); EndpointGuards.RequireCollectionAccess(ctx.User, collection, isWrite: false); var spec = QueryTranslator.Parse(ctx.Request.Query["q"]); var docs = await repo.FindAsync(collection, spec.Filter, spec.Sort, spec.Limit, ct); return BsonResults.DocumentArray(docs); }); group.MapGet("/{collection}/{id}", async (string collection, string id, HttpContext ctx, IDocumentRepository repo, CancellationToken ct) => { EnsureKnown(collection); EndpointGuards.RequireCollectionAccess(ctx.User, collection, isWrite: false); var doc = await repo.GetByIdAsync(collection, id, ct) ?? throw ApiException.NotFound("Not found."); return BsonResults.Document(doc); }); group.MapPost("/{collection}", async (string collection, HttpContext ctx, IDocumentRepository repo, CancellationToken ct) => { EnsureKnown(collection); EndpointGuards.RequireCollectionAccess(ctx.User, collection, isWrite: true); var body = await ctx.Request.ReadBsonBodyAsync(ct); var id = ExtractId(body) ?? DocumentRepository.NewId(); var created = await repo.UpsertAsync(collection, id, body, setUpdatedAt: false, ct); return BsonResults.Document(created, 201); }); group.MapPut("/{collection}/{id}", async (string collection, string id, HttpContext ctx, IDocumentRepository repo, CancellationToken ct) => { EnsureKnown(collection); EndpointGuards.RequireCollectionAccess(ctx.User, collection, isWrite: true); var body = await ctx.Request.ReadBsonBodyAsync(ct); var doc = await repo.UpsertAsync(collection, id, body, setUpdatedAt: true, ct); return BsonResults.Document(doc); }); group.MapPatch("/{collection}/{id}", async (string collection, string id, HttpContext ctx, IDocumentRepository repo, CancellationToken ct) => { EnsureKnown(collection); EndpointGuards.RequireCollectionAccess(ctx.User, collection, isWrite: true); var body = await ctx.Request.ReadBsonBodyAsync(ct); if (!await repo.UpdateAsync(collection, id, body, ct)) throw ApiException.NotFound("Not found."); var doc = await repo.GetByIdAsync(collection, id, ct) ?? throw ApiException.NotFound("Not found."); return BsonResults.Document(doc); }); group.MapDelete("/{collection}/{id}", async (string collection, string id, HttpContext ctx, IDocumentRepository repo, CancellationToken ct) => { EnsureKnown(collection); EndpointGuards.RequireCollectionAccess(ctx.User, collection, isWrite: true); await repo.DeleteAsync(collection, id, ct); return BsonResults.Ok(); }); return app; } private static void EnsureKnown(string collection) { if (!Collections.IsKnown(collection)) throw ApiException.NotFound($"Unknown collection: {collection}"); } private static string? ExtractId(BsonDocument body) { foreach (var key in new[] { "_id", "id", "uid" }) { if (body.TryGetValue(key, out var value) && value.IsString && !string.IsNullOrEmpty(value.AsString)) return value.AsString; } return null; } }