Skip to content

[Backport v8] Improve performance of contentScopesCount via a request-scoped DataLoader - #6144

Merged
VPS-thodax merged 1 commit into
v8.x.xfrom
backport-v8-content-scopes-count-dataloader
Aug 13, 2026
Merged

[Backport v8] Improve performance of contentScopesCount via a request-scoped DataLoader#6144
VPS-thodax merged 1 commit into
v8.x.xfrom
backport-v8-content-scopes-count-dataloader

Conversation

@VPS-thodax

Copy link
Copy Markdown
Contributor

Backport of #6113 to v8.x.x.

Note: the cherry-picked test file (user-permissions.service.spec.ts) was converted from vitest to jest, since this branch still uses jest for @comet/cms-api. No other changes were needed on top of the cherry-pick.

Verification on this branch:

  • pnpm --filter '@comet/cms-api' run build — passes
  • pnpm run lint (in packages/api/cms-api) — passes
  • pnpm exec jest src/user-permissions/user-permissions.service.spec.ts — passes
  • demo/api boots and initializes AppModule/GraphQLModule correctly with the change (verified up to the expected Postgres connection error, since this sandbox can't pull Docker images to run the full demo)

Generated by Claude Code

…ader (#6113)

## Problem

When the admin queries the users list (`userPermissionsUsers`), the
`contentScopesCount` field resolver runs **once per row**. Each
invocation calls `getContentScopes(user)`, which recomputes the full
list of available content scopes (build labels + `uniqWith`
deduplication) from scratch. For a page of users this repeats the same
shared computation N times, which becomes very slow when a project
configures many content scopes.

## Solution

Resolve the available content scopes **once per request** by routing
`contentScopesCount` through a request-scoped `DataLoader`
(`UserContentScopesLoaderService`), following the existing DataLoader
pattern in cms-api (e.g. `AttachedDocumentLoaderService`). All users of
a request are batched into a single `getContentScopesForUsers(users)`
call that computes the available content scopes only once and then maps
each user's scopes.

This PR **keeps the existing `lodash.uniqWith` deduplication** unchanged
— the only change is *how often* the available content scopes are
computed.

## Speed comparison (25 users)

Measured by resolving `contentScopesCount` for 25 users (median of 3
runs), comparing the current per-row behavior against the DataLoader:

**Typical case — each user resolves to a few content scopes**

| Available content scopes | Current (per row) | DataLoader | Speedup |
| ------------------------ | ----------------- | ---------- | ------- |
| 250                      | 359 ms            | 15 ms      | ~25×    |
| 500                      | 1 377 ms          | 55 ms      | ~25×    |
| 1 000                    | 5 400 ms          | 209 ms     | ~26×    |
| 2 000                    | 20 738 ms         | 821 ms     | ~25×    |

**Worst case — every user resolves to *all* content scopes** (the
per-user `uniqWith`, which this PR does not change, then dominates in
both variants):

| Available content scopes | Current (per row) | DataLoader | Speedup |
| ------------------------ | ----------------- | ---------- | ------- |
| 250                      | 558 ms            | 268 ms     | ~2.1×   |
| 500                      | 2 229 ms          | 998 ms     | ~2.2×   |
| 1 000                    | 9 305 ms          | 4 075 ms   | ~2.3×   |
| 2 000                    | 35 057 ms         | 15 771 ms  | ~2.2×   |

So the DataLoader removes 24 of 25 redundant available-content-scope
computations per request; the remaining cost is the per-user work, which
is unchanged.

## Example / test

`user-permissions.service.spec.ts` covers the batching guarantee
(available content scopes computed only once for all users) and that
each user's scopes are returned in input order.

## Notes

Supersedes #5999 (which additionally replaced the `uniqWith`
deduplication with a custom algorithm). This PR isolates just the
DataLoader change.

https://claude.ai/code/session_01RGR2hkqpqcXL4xk7rGqz7n

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit 8c6be74)
@VPS-thodax VPS-thodax self-assigned this Aug 7, 2026
@VPS-thodax
VPS-thodax marked this pull request as ready for review August 7, 2026 07:44
@VPS-thodax
VPS-thodax requested a review from fraxachun August 7, 2026 07:44
@VPS-thodax
VPS-thodax marked this pull request as draft August 7, 2026 07:45
@VPS-thodax
VPS-thodax marked this pull request as ready for review August 7, 2026 07:46
@VPS-thodax
VPS-thodax requested a review from VPS-Obi August 7, 2026 07:47
@VPS-Obi

VPS-Obi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Needs human review — touches permission-related core logic.

Refactors UserPermissionsService.getContentScopes into a shared filterContentScopesForUser helper and adds a request-scoped DataLoader (UserContentScopesLoaderService) so contentScopesCount resolves each user's scopes once per request instead of once per row. It's a tested backport of an already-merged main-branch change (#6113) with new unit coverage (user-permissions.service.spec.ts), which lowers the risk, but changes to user-permissions.service.ts are worth a second pair of eyes given it's on the authorization path.


Generated by Claude Code

@VPS-thodax
VPS-thodax merged commit ab940c1 into v8.x.x Aug 13, 2026
2 checks passed
@VPS-thodax
VPS-thodax deleted the backport-v8-content-scopes-count-dataloader branch August 13, 2026 09:03
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.

3 participants