w4c-workflows-api/Services/Nodes/Executors/HttpRequestNodeExecutor.Pagination.cs

149 lines
4.9 KiB
C#
Raw Permalink Normal View History

2026-09-13 16:28:47 +00:00
using System.Globalization;
using System.Text.Json.Nodes;
using w4c_workflows.Models.Credentials;
using w4c_workflows.Models.Nodes;
using w4c_workflows.Services.Credentials;
using w4c_workflows.Services.Nodes.Binary;
using w4c_workflows.Services.Security;
namespace w4c_workflows.Services.Nodes.Executors;
// Pagination half of the HTTP Request node: turns the declarative plan into
// per-page cursor/page-number/next-URL steps and merges query/header/body
// overrides into the next request.
public sealed partial class HttpRequestNodeExecutor
{
// ------------------------------------------------------------------ pagination
private static JsonNode? ApplyStep(
HttpPaginationStep step,
HttpPaginationPlan plan,
JsonNode? body,
Dictionary<string, string> queryOverrides,
Dictionary<string, string> headerOverrides)
{
if (step.Cursor != null)
return ApplyParameter(plan.CursorPlace, plan.CursorName, step.Cursor, body, queryOverrides, headerOverrides);
if (step.PageNumber is int pageNumber)
return ApplyParameter(
plan.PagePlace, plan.PageName,
pageNumber.ToString(CultureInfo.InvariantCulture),
body, queryOverrides, headerOverrides);
return body;
}
private static JsonNode? ApplyParameter(
string place,
string name,
string value,
JsonNode? body,
Dictionary<string, string> queryOverrides,
Dictionary<string, string> headerOverrides)
{
if (string.IsNullOrEmpty(name))
return body;
switch (place)
{
case HttpPaginationPlan.PlaceHeader:
headerOverrides[name] = value;
return body;
case HttpPaginationPlan.PlaceBody:
if (body is JsonObject obj)
obj[name] = value;
else if (body == null)
body = new JsonObject { [name] = value };
return body;
default:
queryOverrides[name] = value;
return body;
}
}
/// <summary>Merges pagination overrides into the base URL query (same-name keys are replaced).</summary>
private static Uri ApplyQuery(Uri baseUri, IReadOnlyDictionary<string, string> overrides)
{
if (overrides.Count == 0)
return baseUri;
var pairs = new List<KeyValuePair<string, string>>();
if (!string.IsNullOrEmpty(baseUri.Query))
{
foreach (var part in baseUri.Query.TrimStart('?')
.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var separator = part.IndexOf('=');
var key = separator < 0 ? part : part[..separator];
var value = separator < 0 ? string.Empty : part[(separator + 1)..];
pairs.Add(new KeyValuePair<string, string>(
Uri.UnescapeDataString(key), Uri.UnescapeDataString(value)));
}
}
foreach (var (name, value) in overrides)
{
pairs.RemoveAll(pair => string.Equals(pair.Key, name, StringComparison.Ordinal));
pairs.Add(new KeyValuePair<string, string>(name, value));
}
var query = string.Join('&', pairs.Select(pair =>
$"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}"));
return new UriBuilder(baseUri) { Query = query }.Uri;
}
/// <summary>Resolves a pagination next URL (absolute or relative) and keeps it HTTP(S).</summary>
private static bool TryResolveNext(string nextUrl, Uri current, out Uri next)
{
next = null!;
try
{
// A leading-slash path parses as an absolute "file" URI on Unix, so
// only an explicitly http(s) absolute URL bypasses relative resolution.
if (Uri.TryCreate(nextUrl, UriKind.Absolute, out var absolute)
&& absolute.Scheme is "http" or "https")
{
next = absolute;
}
else
{
next = new Uri(current, nextUrl);
}
return next.Scheme is "http" or "https";
}
catch (UriFormatException)
{
return false;
}
}
/// <summary>Accumulates a page under the item cap; returns true when the cap is reached.</summary>
private static bool AddItems(List<FlowItem> items, IReadOnlyList<FlowItem> pageItems, int maxItems)
{
if (maxItems <= 0)
{
items.AddRange(pageItems);
return false;
}
var remaining = maxItems - items.Count;
if (remaining <= 0)
return true;
if (pageItems.Count > remaining)
{
items.AddRange(pageItems.Take(remaining));
return true;
}
items.AddRange(pageItems);
return items.Count >= maxItems;
}
}