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:
@@ -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 > 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.
|
||||
|
||||
@@ -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 > 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",
|
||||
|
||||
@@ -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 > 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.
|
||||
|
||||
@@ -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 > 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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user