149 lines
4.9 KiB
C#
149 lines
4.9 KiB
C#
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;
|
|
}
|
|
}
|