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>
|
/// <summary>Pagination: max rows to return. 0 = return all.</summary>
|
||||||
public int Take { get; init; }
|
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>
|
/// <summary>
|
||||||
/// Optional sort. SortBy is whitelisted per-endpoint — for definitions the allowed values are
|
/// Optional sort. SortBy is whitelisted per-endpoint — for definitions the allowed values are
|
||||||
/// "workflowName", "workflowVersion", "displayName". Null sorts by a stable default.
|
/// "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>
|
/// <summary>Pagination: max rows to return. 0 = use server default cap.</summary>
|
||||||
public int Take { get; init; }
|
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>
|
/// <summary>
|
||||||
/// Optional sort. SortBy is whitelisted per-endpoint — for instances the allowed values are
|
/// Optional sort. SortBy is whitelisted per-endpoint — for instances the allowed values are
|
||||||
/// "workflowInstanceId", "workflowName", "workflowVersion", "status", "createdOnUtc",
|
/// "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>
|
/// <summary>Pagination: max rows to return. 0 = use <see cref="MaxMessages"/> as the effective cap.</summary>
|
||||||
public int Take { get; init; }
|
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>
|
/// <summary>
|
||||||
/// Optional sort. SortBy is whitelisted — allowed values are "signalId", "workflowInstanceId",
|
/// Optional sort. SortBy is whitelisted — allowed values are "signalId", "workflowInstanceId",
|
||||||
/// "signalType", "enqueuedOnUtc", "deliveryCount". Null sorts by enqueuedOnUtc desc.
|
/// "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>
|
/// <summary>Pagination: max rows to return. 0 = use server default cap.</summary>
|
||||||
public int Take { get; init; }
|
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>
|
/// <summary>
|
||||||
/// Optional sort. SortBy is whitelisted per-endpoint — for tasks the allowed values are
|
/// Optional sort. SortBy is whitelisted per-endpoint — for tasks the allowed values are
|
||||||
/// "workflowTaskId", "taskName", "workflowName", "workflowVersion", "status", "assignee",
|
/// "workflowTaskId", "taskName", "workflowName", "workflowVersion", "status", "assignee",
|
||||||
|
|||||||
@@ -284,9 +284,9 @@ public sealed class MongoWorkflowSignalStore(
|
|||||||
filter &= Builders<WorkflowSignalDocument>.Filter.Eq(x => x.SignalType, request.SignalType);
|
filter &= Builders<WorkflowSignalDocument>.Filter.Eq(x => x.SignalType, request.SignalType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var effectiveTake = request.Take > 0 ? request.Take : request.MaxMessages;
|
var (effectiveSkip, effectiveTake) = (request.Page > 0 && request.PageSize > 0)
|
||||||
effectiveTake = Math.Clamp(effectiveTake, 1, 500);
|
? (Math.Max(0, (request.Page - 1) * request.PageSize), Math.Clamp(request.PageSize, 1, 500))
|
||||||
var effectiveSkip = Math.Max(0, request.Skip);
|
: (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".
|
// Count for the response envelope so the UI can render "page X of Y".
|
||||||
var totalCount = (int)Math.Min(int.MaxValue,
|
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.
|
// Resolve an ORDER BY clause from the whitelist — never interpolate user input into SQL.
|
||||||
var orderBy = ResolveDeadLetterOrderBy(request.Sort);
|
var orderBy = ResolveDeadLetterOrderBy(request.Sort);
|
||||||
|
|
||||||
// Effective cap: use Take when given; otherwise MaxMessages. Hard-cap at 500 so a slow
|
// Effective cap. Precedence: Page/PageSize (sc-table-view) first, then Skip/Take, then
|
||||||
// driver can't be asked for an unbounded result set.
|
// MaxMessages as a safety cap. Hard-cap at 500 so a slow driver can't be asked for an
|
||||||
var effectiveTake = request.Take > 0 ? request.Take : request.MaxMessages;
|
// unbounded result set.
|
||||||
effectiveTake = Math.Clamp(effectiveTake, 1, 500);
|
var (effectiveSkip, effectiveTake) = (request.Page > 0 && request.PageSize > 0)
|
||||||
var effectiveSkip = Math.Max(0, request.Skip);
|
? (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);
|
await using var scope = await database.OpenScopeAsync(requireTransaction: false, cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -156,11 +156,11 @@ public sealed class WorkflowProjectionStore(
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Honour Skip/Take when the client asked for a page (0 means "all").
|
// Honour Page/PageSize (sc-table-view convention) first, falling back to Skip/Take.
|
||||||
if (request.Skip > 0 || request.Take > 0)
|
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(skip).Take(take).ToArray();
|
||||||
summaries = summaries.Skip(request.Skip).Take(take).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return summaries;
|
return summaries;
|
||||||
@@ -528,10 +528,10 @@ public sealed class WorkflowProjectionStore(
|
|||||||
.Where(x => x.BusinessReference.MatchesBusinessReferenceFilter(businessReferenceKey, request.BusinessReferenceParts))
|
.Where(x => x.BusinessReference.MatchesBusinessReferenceFilter(businessReferenceKey, request.BusinessReferenceParts))
|
||||||
.ToArray();
|
.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(iSkip).Take(iTake).ToArray();
|
||||||
filtered = filtered.Skip(request.Skip).Take(take).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
@@ -770,6 +770,21 @@ public sealed class WorkflowProjectionStore(
|
|||||||
return JsonSerializer.Serialize(value, SerializerOptions);
|
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)
|
private static string? SerializeBusinessReference(WorkflowBusinessReference? businessReference)
|
||||||
{
|
{
|
||||||
var normalizedReference = WorkflowBusinessReferenceExtensions.NormalizeBusinessReference(businessReference);
|
var normalizedReference = WorkflowBusinessReferenceExtensions.NormalizeBusinessReference(businessReference);
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ public sealed class OracleAqWorkflowSignalDeadLetterStore(
|
|||||||
var sorted = ApplyInMemorySort(filtered, request.Sort);
|
var sorted = ApplyInMemorySort(filtered, request.Sort);
|
||||||
|
|
||||||
var totalCount = sorted.Count;
|
var totalCount = sorted.Count;
|
||||||
var effectiveTake = request.Take > 0 ? request.Take : safetyCap;
|
// Page/PageSize (sc-table-view) first, then Skip/Take, then safetyCap fallback.
|
||||||
effectiveTake = Math.Clamp(effectiveTake, 1, 500);
|
var (effectiveSkip, effectiveTake) = (request.Page > 0 && request.PageSize > 0)
|
||||||
var effectiveSkip = Math.Max(0, request.Skip);
|
? (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();
|
var page = sorted.Skip(effectiveSkip).Take(effectiveTake).ToArray();
|
||||||
|
|
||||||
return new WorkflowSignalDeadLettersGetResponse
|
return new WorkflowSignalDeadLettersGetResponse
|
||||||
|
|||||||
Reference in New Issue
Block a user