feat(workflow): accept sc-table-view Page/PageSize body keys (backport)

- Contracts: four list requests + dead-letter request gain optional `Page` and
  `PageSize` (1-based) alongside existing `Skip`/`Take`. When both are > 0 the
  server derives `Skip = (Page - 1) * PageSize` and `Take = PageSize`, taking
  precedence over explicit Skip/Take. Matches the payload shape sc-table-view
  emits natively, so clients don't need a beforeRequest shim to compute skip/take.
- Projection store's GetTasksAsync / GetInstancesAsync gain a `ResolveSkipTake`
  helper with the new precedence. Dead-letter drivers (Postgres, MongoDB,
  OracleAq) apply the same precedence at the top of `GetDeadLettersAsync` /
  `GetMessagesAsync`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-14 12:46:09 +03:00
parent 6ec6c4ebea
commit c69ebb4c48
8 changed files with 75 additions and 18 deletions

View File

@@ -33,6 +33,16 @@ public sealed record WorkflowDefinitionGetRequest
/// <summary>Pagination: max rows to return. 0 = return all.</summary>
public int Take { get; init; }
/// <summary>
/// Pagination (1-based) alternative that matches the sc-table-view payload convention.
/// When both are &gt; 0 the server derives <c>Skip = (Page - 1) * PageSize</c> and
/// <c>Take = PageSize</c>, taking precedence over explicit Skip/Take.
/// </summary>
public int Page { get; init; }
/// <summary>See <see cref="Page"/>.</summary>
public int PageSize { get; init; }
/// <summary>
/// Optional sort. SortBy is whitelisted per-endpoint — for definitions the allowed values are
/// "workflowName", "workflowVersion", "displayName". Null sorts by a stable default.

View File

@@ -47,6 +47,16 @@ public sealed record WorkflowInstancesGetRequest
/// <summary>Pagination: max rows to return. 0 = use server default cap.</summary>
public int Take { get; init; }
/// <summary>
/// Pagination (1-based) alternative that matches the sc-table-view payload convention.
/// When both are &gt; 0 the server derives <c>Skip = (Page - 1) * PageSize</c> and
/// <c>Take = PageSize</c>, taking precedence over explicit Skip/Take.
/// </summary>
public int Page { get; init; }
/// <summary>See <see cref="Page"/>.</summary>
public int PageSize { get; init; }
/// <summary>
/// Optional sort. SortBy is whitelisted per-endpoint — for instances the allowed values are
/// "workflowInstanceId", "workflowName", "workflowVersion", "status", "createdOnUtc",

View File

@@ -91,6 +91,16 @@ public sealed record WorkflowSignalDeadLettersGetRequest
/// <summary>Pagination: max rows to return. 0 = use <see cref="MaxMessages"/> as the effective cap.</summary>
public int Take { get; init; }
/// <summary>
/// Pagination (1-based) alternative that matches the sc-table-view payload convention.
/// When both are &gt; 0 the server derives <c>Skip = (Page - 1) * PageSize</c> and
/// <c>Take = PageSize</c>, taking precedence over explicit Skip/Take.
/// </summary>
public int Page { get; init; }
/// <summary>See <see cref="Page"/>.</summary>
public int PageSize { get; init; }
/// <summary>
/// Optional sort. SortBy is whitelisted — allowed values are "signalId", "workflowInstanceId",
/// "signalType", "enqueuedOnUtc", "deliveryCount". Null sorts by enqueuedOnUtc desc.

View File

@@ -63,6 +63,16 @@ public sealed record WorkflowTasksGetRequest
/// <summary>Pagination: max rows to return. 0 = use server default cap.</summary>
public int Take { get; init; }
/// <summary>
/// Pagination (1-based) alternative that matches the sc-table-view payload convention.
/// When both are &gt; 0 the server derives <c>Skip = (Page - 1) * PageSize</c> and
/// <c>Take = PageSize</c>, taking precedence over explicit Skip/Take.
/// </summary>
public int Page { get; init; }
/// <summary>See <see cref="Page"/>.</summary>
public int PageSize { get; init; }
/// <summary>
/// Optional sort. SortBy is whitelisted per-endpoint — for tasks the allowed values are
/// "workflowTaskId", "taskName", "workflowName", "workflowVersion", "status", "assignee",

View File

@@ -284,9 +284,9 @@ public sealed class MongoWorkflowSignalStore(
filter &= Builders<WorkflowSignalDocument>.Filter.Eq(x => x.SignalType, request.SignalType);
}
var effectiveTake = request.Take > 0 ? request.Take : request.MaxMessages;
effectiveTake = Math.Clamp(effectiveTake, 1, 500);
var effectiveSkip = Math.Max(0, request.Skip);
var (effectiveSkip, effectiveTake) = (request.Page > 0 && request.PageSize > 0)
? (Math.Max(0, (request.Page - 1) * request.PageSize), Math.Clamp(request.PageSize, 1, 500))
: (Math.Max(0, request.Skip), Math.Clamp(request.Take > 0 ? request.Take : request.MaxMessages, 1, 500));
// Count for the response envelope so the UI can render "page X of Y".
var totalCount = (int)Math.Min(int.MaxValue,

View File

@@ -308,11 +308,12 @@ public sealed class PostgresWorkflowSignalStore(
// Resolve an ORDER BY clause from the whitelist — never interpolate user input into SQL.
var orderBy = ResolveDeadLetterOrderBy(request.Sort);
// Effective cap: use Take when given; otherwise MaxMessages. Hard-cap at 500 so a slow
// driver can't be asked for an unbounded result set.
var effectiveTake = request.Take > 0 ? request.Take : request.MaxMessages;
effectiveTake = Math.Clamp(effectiveTake, 1, 500);
var effectiveSkip = Math.Max(0, request.Skip);
// Effective cap. Precedence: Page/PageSize (sc-table-view) first, then Skip/Take, then
// MaxMessages as a safety cap. Hard-cap at 500 so a slow driver can't be asked for an
// unbounded result set.
var (effectiveSkip, effectiveTake) = (request.Page > 0 && request.PageSize > 0)
? (Math.Max(0, (request.Page - 1) * request.PageSize), Math.Clamp(request.PageSize, 1, 500))
: (Math.Max(0, request.Skip), Math.Clamp(request.Take > 0 ? request.Take : request.MaxMessages, 1, 500));
await using var scope = await database.OpenScopeAsync(requireTransaction: false, cancellationToken);

View File

@@ -156,11 +156,11 @@ public sealed class WorkflowProjectionStore(
.ToArray();
}
// Honour Skip/Take when the client asked for a page (0 means "all").
if (request.Skip > 0 || request.Take > 0)
// Honour Page/PageSize (sc-table-view convention) first, falling back to Skip/Take.
var (skip, take) = ResolveSkipTake(request.Skip, request.Take, request.Page, request.PageSize, summaries.Length);
if (skip > 0 || take < summaries.Length)
{
var take = request.Take > 0 ? request.Take : summaries.Length;
summaries = summaries.Skip(request.Skip).Take(take).ToArray();
summaries = summaries.Skip(skip).Take(take).ToArray();
}
return summaries;
@@ -528,10 +528,10 @@ public sealed class WorkflowProjectionStore(
.Where(x => x.BusinessReference.MatchesBusinessReferenceFilter(businessReferenceKey, request.BusinessReferenceParts))
.ToArray();
if (request.Skip > 0 || request.Take > 0)
var (iSkip, iTake) = ResolveSkipTake(request.Skip, request.Take, request.Page, request.PageSize, filtered.Length);
if (iSkip > 0 || iTake < filtered.Length)
{
var take = request.Take > 0 ? request.Take : filtered.Length;
filtered = filtered.Skip(request.Skip).Take(take).ToArray();
filtered = filtered.Skip(iSkip).Take(iTake).ToArray();
}
return filtered;
@@ -770,6 +770,21 @@ public sealed class WorkflowProjectionStore(
return JsonSerializer.Serialize(value, SerializerOptions);
}
/// <summary>
/// Resolves the effective <c>(skip, take)</c> pair from the request. Page/PageSize
/// (sc-table-view convention, 1-based) take precedence over explicit Skip/Take when provided.
/// </summary>
private static (int Skip, int Take) ResolveSkipTake(int requestSkip, int requestTake, int requestPage, int requestPageSize, int totalLength)
{
if (requestPage > 0 && requestPageSize > 0)
{
return (Math.Max(0, (requestPage - 1) * requestPageSize), requestPageSize);
}
var take = requestTake > 0 ? requestTake : totalLength;
return (Math.Max(0, requestSkip), take);
}
private static string? SerializeBusinessReference(WorkflowBusinessReference? businessReference)
{
var normalizedReference = WorkflowBusinessReferenceExtensions.NormalizeBusinessReference(businessReference);

View File

@@ -55,9 +55,10 @@ public sealed class OracleAqWorkflowSignalDeadLetterStore(
var sorted = ApplyInMemorySort(filtered, request.Sort);
var totalCount = sorted.Count;
var effectiveTake = request.Take > 0 ? request.Take : safetyCap;
effectiveTake = Math.Clamp(effectiveTake, 1, 500);
var effectiveSkip = Math.Max(0, request.Skip);
// Page/PageSize (sc-table-view) first, then Skip/Take, then safetyCap fallback.
var (effectiveSkip, effectiveTake) = (request.Page > 0 && request.PageSize > 0)
? (Math.Max(0, (request.Page - 1) * request.PageSize), Math.Clamp(request.PageSize, 1, 500))
: (Math.Max(0, request.Skip), Math.Clamp(request.Take > 0 ? request.Take : safetyCap, 1, 500));
var page = sorted.Skip(effectiveSkip).Take(effectiveTake).ToArray();
return new WorkflowSignalDeadLettersGetResponse