From c69ebb4c48c5c5688c8d6f03c6ef7d84ca3e1a10 Mon Sep 17 00:00:00 2001 From: master <> Date: Tue, 14 Apr 2026 12:46:09 +0300 Subject: [PATCH] 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) --- .../WorkflowDefinitionsContracts.cs | 10 +++++++ .../WorkflowInstanceContracts.cs | 10 +++++++ .../WorkflowOperationalContracts.cs | 10 +++++++ .../WorkflowTaskContracts.cs | 10 +++++++ .../MongoWorkflowSignalStore.cs | 6 ++-- .../PostgresWorkflowSignalStore.cs | 11 +++---- .../Projections/WorkflowProjectionStore.cs | 29 ++++++++++++++----- .../OracleAqWorkflowSignalDeadLetterStore.cs | 7 +++-- 8 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowDefinitionsContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowDefinitionsContracts.cs index a3d59ce14..a43d0846c 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowDefinitionsContracts.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowDefinitionsContracts.cs @@ -33,6 +33,16 @@ public sealed record WorkflowDefinitionGetRequest /// Pagination: max rows to return. 0 = return all. public int Take { get; init; } + /// + /// Pagination (1-based) alternative that matches the sc-table-view payload convention. + /// When both are > 0 the server derives Skip = (Page - 1) * PageSize and + /// Take = PageSize, taking precedence over explicit Skip/Take. + /// + public int Page { get; init; } + + /// See . + public int PageSize { get; init; } + /// /// Optional sort. SortBy is whitelisted per-endpoint — for definitions the allowed values are /// "workflowName", "workflowVersion", "displayName". Null sorts by a stable default. diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowInstanceContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowInstanceContracts.cs index 97129b20e..8e00111cb 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowInstanceContracts.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowInstanceContracts.cs @@ -47,6 +47,16 @@ public sealed record WorkflowInstancesGetRequest /// Pagination: max rows to return. 0 = use server default cap. public int Take { get; init; } + /// + /// Pagination (1-based) alternative that matches the sc-table-view payload convention. + /// When both are > 0 the server derives Skip = (Page - 1) * PageSize and + /// Take = PageSize, taking precedence over explicit Skip/Take. + /// + public int Page { get; init; } + + /// See . + public int PageSize { get; init; } + /// /// Optional sort. SortBy is whitelisted per-endpoint — for instances the allowed values are /// "workflowInstanceId", "workflowName", "workflowVersion", "status", "createdOnUtc", diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowOperationalContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowOperationalContracts.cs index 0e4f60698..62777e3f8 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowOperationalContracts.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowOperationalContracts.cs @@ -91,6 +91,16 @@ public sealed record WorkflowSignalDeadLettersGetRequest /// Pagination: max rows to return. 0 = use as the effective cap. public int Take { get; init; } + /// + /// Pagination (1-based) alternative that matches the sc-table-view payload convention. + /// When both are > 0 the server derives Skip = (Page - 1) * PageSize and + /// Take = PageSize, taking precedence over explicit Skip/Take. + /// + public int Page { get; init; } + + /// See . + public int PageSize { get; init; } + /// /// Optional sort. SortBy is whitelisted — allowed values are "signalId", "workflowInstanceId", /// "signalType", "enqueuedOnUtc", "deliveryCount". Null sorts by enqueuedOnUtc desc. diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs index a1b5ed9b4..14ef2419f 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs @@ -63,6 +63,16 @@ public sealed record WorkflowTasksGetRequest /// Pagination: max rows to return. 0 = use server default cap. public int Take { get; init; } + /// + /// Pagination (1-based) alternative that matches the sc-table-view payload convention. + /// When both are > 0 the server derives Skip = (Page - 1) * PageSize and + /// Take = PageSize, taking precedence over explicit Skip/Take. + /// + public int Page { get; init; } + + /// See . + public int PageSize { get; init; } + /// /// Optional sort. SortBy is whitelisted per-endpoint — for tasks the allowed values are /// "workflowTaskId", "taskName", "workflowName", "workflowVersion", "status", "assignee", diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.MongoDB/MongoWorkflowSignalStore.cs b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.MongoDB/MongoWorkflowSignalStore.cs index d6171c087..346b8ac51 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.MongoDB/MongoWorkflowSignalStore.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.MongoDB/MongoWorkflowSignalStore.cs @@ -284,9 +284,9 @@ public sealed class MongoWorkflowSignalStore( filter &= Builders.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, diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowSignalStore.cs b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowSignalStore.cs index 8dfa4a906..f95220272 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowSignalStore.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowSignalStore.cs @@ -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); diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Engine/Projections/WorkflowProjectionStore.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Engine/Projections/WorkflowProjectionStore.cs index 75f1719bf..7f5e02f31 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Engine/Projections/WorkflowProjectionStore.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Engine/Projections/WorkflowProjectionStore.cs @@ -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); } + /// + /// Resolves the effective (skip, take) pair from the request. Page/PageSize + /// (sc-table-view convention, 1-based) take precedence over explicit Skip/Take when provided. + /// + 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); diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Signaling.OracleAq/OracleAqWorkflowSignalDeadLetterStore.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Signaling.OracleAq/OracleAqWorkflowSignalDeadLetterStore.cs index e40ab4ac0..b49b47c6c 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Signaling.OracleAq/OracleAqWorkflowSignalDeadLetterStore.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Signaling.OracleAq/OracleAqWorkflowSignalDeadLetterStore.cs @@ -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