using EAIAdminBLL.EAIExecution; using EAIAdminDAL.DTO; using FastEndpoints; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.FastEndPoint; using System.IO; using System.Linq; using static GB5Shared.GB5Constant.Constant; namespace EAIAdminSL.Endpoints { // The one generalized entry point every functional-module caller uses to reach gbEAI through // EAIAdmin's governance layer — see EAIExecuteCapabilityRequestDTO's doc comment. // // Two distinct callers, two distinct body shapes, both supported here: // - GopDocumentIntakeBLL calls directly with a multipart/form-data body carrying just the // file (IDENTIFY_DOCUMENT/PARSE_VALIDATE_DOCUMENT — both requires_file=True on gbEAI's // side, no InputJson needed). // - A GOP flow's ApiCall step (ApiCallNodeExecutor) always sends the pipeline's JSON payload // as a plain application/json request body — it has no mechanism to encode a per-execution // value into a query string. InitiativeCode/CapabilityCode/VariantCode stay QueryParam // (static values an ApiCall step's RelativePath can embed directly, e.g. // "/EAIAdmin/ExecuteCapability?InitiativeCode=RECRUITMENT&CapabilityCode=GENERATE_PROFILE"); // InputJson is read from the raw request body instead, never from a query param — a query // param can't carry an arbitrary caller-supplied JSON payload without a size/encoding limit // that "the current pipeline payload, whatever shape it is" can't respect. // // Request.HasFormContentType is checked BEFORE touching Request.Form (which lazily parses the // body) so a plain JSON POST is never misread as multipart. public class ExecuteCapability : BaseEndpoint> { private readonly IEAIExecutionBLL _bll; public ExecuteCapability(IEAIExecutionBLL bll) => _bll = bll; public override void Configure() { Post("/EAIAdmin/ExecuteCapability"); AllowAnonymous(); // AllowFileUploads() replaces (not adds to) the endpoint's accepted content type with // multipart/form-data only, which 415s a GOP ApiCall step's plain application/json // body — this endpoint genuinely serves both caller shapes, so declare both content // types explicitly instead. File reading below uses plain HttpContext.Request.Form, // unrelated to FastEndpoints' own AllowFileUploads plumbing. Options(x => x.Accepts("application/json", "multipart/form-data")); Description(b => b.WithTags("EAIAdmin")); } protected override string? GetCacheKey(Parameters req, LoginDTO login) => null; protected override async Task> ExecuteAsync( Parameters req, LoginDTO login, CancellationToken ct) { (string FileName, byte[] Bytes)? file = null; string? inputJson = null; if (HttpContext.Request.HasFormContentType) { var formFile = HttpContext.Request.Form.Files.FirstOrDefault(); if (formFile is not null && formFile.Length > 0) { await using var stream = formFile.OpenReadStream(); using var ms = new MemoryStream(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); file = (formFile.FileName, ms.ToArray()); } } else if (HttpContext.Request.ContentLength is > 0) { // FastEndpoints' own request-binding pass reads the body stream first (looking // for a JSON payload to bind onto Parameters, even though none of its properties // are body-bound) — without rewinding, this manual read sees an already-exhausted // stream and silently returns "". EnableBuffering + explicit Position=0 makes the // body re-readable here. HttpContext.Request.EnableBuffering(); HttpContext.Request.Body.Position = 0; using var reader = new StreamReader(HttpContext.Request.Body, leaveOpen: true); inputJson = await reader.ReadToEndAsync(ct).ConfigureAwait(false); } var dto = new EAIExecuteCapabilityRequestDTO { InitiativeCode = req.InitiativeCode, CapabilityCode = req.CapabilityCode, VariantCode = req.VariantCode, InputJson = inputJson }; var result = await _bll.ExecuteCapabilityAsync(dto, login, file, ct).ConfigureAwait(false); return await GB5Shared.ResponseStandard.Response.CreateSuccessResponse( result, CacheKeyLevel.NOT_REQUIRED, login); } public record Parameters( [property: FromHeader] string? Login, [property: QueryParam] string InitiativeCode, [property: QueryParam] string CapabilityCode, [property: QueryParam] string? VariantCode ); } }