Skip to content

feat(perf): server side paging - #464

Open
Zacgoose wants to merge 10 commits into
devfrom
preview/server-side-paging
Open

feat(perf): server side paging#464
Zacgoose wants to merge 10 commits into
devfrom
preview/server-side-paging

Conversation

@Zacgoose

@Zacgoose Zacgoose commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces significant enhancements to the API's pagination capabilities and improves data handling in several backend PowerShell modules. The main focus is on adding support for manual, token-based pagination across multiple endpoints, updating OpenAPI documentation, and making data storage more robust and deterministic.

API Pagination Enhancements:

  • Added support for manual, token-based pagination (manualPagination, nextLink, PageSize) to multiple API endpoints, including Conditional Access policies, Entra ID groups, guest accounts, Exchange mailboxes, MFA registration status, and CIPP platform audit logs. Descriptions were updated to clarify pagination behavior and response structure. (backend/Config/openapi.json: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
  • Introduced new query parameters such as maxPageBytes (for controlling raw JSON page size) and improved descriptions and filtering options for audit log queries. (backend/Config/openapi.json: [1] [2]

Backend Data Handling Improvements:

Deterministic and Robust Data Storage:

  • Changed the construction of RowKey in Push-ListConditionalAccessPoliciesAllTenants.ps1 to use a deterministic, tenant-and-policy-based key instead of a GUID, preventing duplicate entries during concurrent runs and ensuring idempotency. Error rows now use a similarly deterministic key. (backend/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ListConditionalAccessPoliciesAllTenants.ps1: [1] [2] [3]

…plit entities

Add-CIPPDbItem's orphan sweep projected OriginalEntityId without PartIndex and
PartCount. Get-AzDataTableLargeEntity then recognised a split row but could not
reassemble it, threw IncompleteEntity, and the wrapper skipped the whole entity - so
the sweep never saw the stale split rows it exists to delete, and one uncollectable
generation of rows accumulated per run.

Project all three row-level split markers so split entities reassemble into logical
rows that each carry their RunId, keeping the identity-based orphan check sound;
Remove-CIPPAzDataTableEntity then deletes every part row. SplitOverProps and the Data
chunk columns are deliberately left out: the sweep needs identity, not payload.

Fixes #462
Add continuation-token paging for report DB-backed guest user, mailbox, MFA, log, and AllTenants Graph cache reads, including shared table paging helpers and frontend support for the { Results, Metadata.nextLink } contract. This keeps large cached datasets usable without loading everything at once, improves log ordering and single-entry lookup, and fixes excluded license defaults so dropdown visibility matches the intended behavior.
Replaces GUID-keyed cache rows with deterministic tenant+policy RowKeys so overlapping fan-outs upsert rather than append duplicates.

Adds a manualPagination query mode to Invoke-ListConditionalAccessPolicies that returns one page per request with a continuation token in Metadata.nextLink, preventing full-table scans on large multi-tenant deployments. PageSize defaults to 5000 and is clamped to [250, 10000].

The cold-cache queue guard is skipped when a nextLink token is present so mid-walk empty pages do not re-trigger the fan-out. The frontend passes manualPagination=true to opt in.

Pester tests cover the idempotent cache writes and the paged/unpaged endpoint branches.
The AllTenants Conditional Access list served cached rows by deserializing
every policy blob into a PowerShell object graph and handing it back for the
host to re-serialize. Each cached Policy blob is already the final flattened
shape the frontend consumes, so stitch the allowed rows straight into the
Results array with a raw application/json body instead, mirroring the raw
passthrough Invoke-ListGraphRequest already uses.

This covers both the paged and legacy unpaged AllTenants reads that share the
tail. Measured on a 5000-policy page: ~1.4x faster and ~2x less allocation,
and it also removes the host-side re-serialization of the whole graph. Tenant
scope filtering still runs at the row level before the stitch; the response is
byte-identical over the wire, so no frontend change is needed. Ordering is now
table-walk order (client sorts), matching the paged groups list.
The paged AllTenants groups list deserialized every cached group blob into a
PowerShell object graph just to derive membersCsv/ownersCsv, decorate it, and
hand it back for the host to re-serialize. On tenants with 50k-member groups
that means materializing tens of thousands of member objects per group on the
read path, which is a large per-request memory spike under load.

Precompute membersCsv/ownersCsv when the group is cached, then serve the page
with a new Get-CIPPGroupsReport -AsRawJson mode that stitches the stored blobs
into the Results array verbatim and splices the two row-level fields
(CacheTimestamp, and Tenant for AllTenants) onto each blob without parsing it.
The member array is never deserialized on the read path.

Measured on a 5x50,000-member page: ~2x faster and ~4.5x less allocated than
the previous read, and the host no longer re-serializes the whole graph. The
response is byte-identical over the wire, so no frontend change is needed.
membersCsv/ownersCsv appear once the groups cache is refreshed; until then the
member/owner sub-tables fall back to their existing on-demand live fetch.

Also fixes a stale test assertion (paged default size is 750, not 1000).
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✅ PR title follows Conventional Commits

Received: feat(perf): server side paging

Thanks for fixing it!

@github-actions github-actions Bot closed this Sep 1, 2026
@Zacgoose Zacgoose changed the title Preview/server side paging feat(perf): server side paging Sep 1, 2026
@github-actions github-actions Bot reopened this Sep 1, 2026
@KelvinTegelaar

Copy link
Copy Markdown
Contributor

brother unit test is angry at us.

Set-CIPPDBCacheGroups now precomputes membersCsv/ownersCsv next to their
source arrays so the paged list read can stream the cached blob verbatim.
Update the documented-order assertion to expect the two new fields, and give
the fixture an owner and a member with a UPN so both CSVs are actually
exercised.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants