Skip to content

fix(cases): resolve descendant cases server-side to avoid HTTP 414 on deep folders#236

Merged
therealbrad merged 2 commits intomainfrom
fix/show-all-descendants-breaks-with-deep-nested-folders
Apr 23, 2026
Merged

fix(cases): resolve descendant cases server-side to avoid HTTP 414 on deep folders#236
therealbrad merged 2 commits intomainfrom
fix/show-all-descendants-breaks-with-deep-nested-folders

Conversation

@therealbrad
Copy link
Copy Markdown
Contributor

Description

Fixes HTTP 414 "URI Too Long" errors when "Show all descendants" is toggled on for a folder with a deep / wide subtree. Previously, the client walked the folder tree, collected every descendant folder ID, and sent them to ZenStack as { folderId: { in: [...hundreds of ids...] } } via a GET request. Deep trees pushed the serialized query string past the server's URI length limit.

The fix keeps descendant resolution server-side via a POST endpoint backed by a recursive CTE, so the wire payload is bounded to a single folderId regardless of tree depth.

Server

  • New endpoint POST /api/projects/[projectId]/cases/by-folder-descendants
    • Resolves the folder subtree with a recursive CTE on RepositoryFolders
    • Enforces projectId + folderId: { in: [descendants] } server-side so the client can't use it to query outside the authorized subtree
    • Runs count + findMany in parallel and returns { cases, totalCount } in one round-trip
    • Authorization goes through getEnhancedDb(session) — ZenStack's @@allow('read') on RepositoryCases is enforced per-row, which is stricter than the hand-rolled project-access clause used by fetch-many / bulk-edit. Closes gaps around access == 'NONE' users with stale userPermissions, GLOBAL_ROLE grants without a role, etc.
    • The recursive CTE stays on raw $queryRaw (SQL is opaque to policies), but it's gated by the enhanced projects.findUnique first and folder read access is schema-derived from project read access, so it's safe.

Client

  • New hook useFindManyRepositoryCasesByDescendants — thin useQuery wrapper that returns the same { data, isLoading, totalCount, refetch } shape as the ZenStack wrapper, including post-fetch text/link/steps filters and client-side pagination
  • Cases.tsx:
    • Extracted the list select into a module-scope REPOSITORY_CASE_LIST_SELECT (single source for both hooks)
    • Added isDescendantsMode flag and repositoryCaseWhereClauseWithoutFolderFilter
    • Disabled the three URL-bound ZenStack hooks (main find, count, Shift+Select-All find) when isDescendantsMode is active; the parallel descendants-POST counterparts take over
    • Conditionally routed data / isLoading / filteredTotalCount / refetchData and totalRepositoryCases through whichever hook is active
    • The export server action path keeps using the original where clause (it's already POST, so URL length isn't a concern)

Related Issue

Closes #(issue number)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

How Has This Been Tested?

  • Unit tests — all 17 Cases.test.tsx tests pass (the existing showDescendants test now exercises the POST-mode path via a mocked descendants hook)
  • Integration tests — useRepositoryCasesWithFilteredFields.integration.test.tsx passes (15/15)
  • E2E tests
  • Manual testing — open a folder with a deep / wide subtree, toggle "Show all descendants"; the 414 no longer fires and the cases list renders (pagination, sort, and Shift+Select-All still work)

Test Configuration:

  • OS: macOS (Darwin 25.4.0)
  • Browser: Chrome
  • Node version: as specified by .nvmrc

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas (recursive CTE, descendants-mode branches, enhanced-db rationale, select-sharing)
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings (ESLint: 0 errors; TS: 0 errors in touched files)
  • I have added tests that prove my fix is effective or that my feature works (updated Cases.test.tsx mock so the existing showDescendants test now covers the POST-mode path)
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have signed the CLA

Screenshots (if applicable)

Additional Notes

  • The POST endpoint accepts the client's where, orderBy, and select as pass-through JSON. projectId and folderId are force-overridden server-side after the client's where is spread, so OR/AND tricks can't escape the subtree or cross projects.
  • descendantFolderIds (computed in ProjectRepository) is still produced client-side: it's used as a signal that descendants mode is active, as input to folderPathMap for displaying relative paths, and by the export server action (which already POSTs, so URL length isn't an issue there).
  • Run-mode paths (useFindManyTestRunCases, useCountTestRunCases) are untouched — "Show all descendants" is a repository-view-only toggle, so run mode never sees a huge folderId array. If run mode ever gains the toggle, the same pattern can be extended.
  • The access-tightening (enhanced DB) is intentionally scoped to this new endpoint. fetch-many and bulk-edit still use the looser inline pattern; migrating them is a separate concern.

therealbrad and others added 2 commits April 22, 2026 15:34
…by POST

- Introduced a new mock for the `useFindManyRepositoryCasesByDescendants` hook in `Cases.test.tsx` to simulate data fetching.
- Updated the `Cases.tsx` component to integrate the new hook, allowing for the retrieval of cases based on descendant folder IDs.
- Enhanced the repository case list query to accommodate both standard and descendant fetching modes, improving the handling of deeply nested folder structures.
- Added a new entry to `.prettierignore` for local GSD planning artifacts.
Switch the by-folder-descendants endpoint from a hand-rolled project-access
clause to getEnhancedDb(), so ZenStack @@Allow('read') runs per-row on the
count + findMany queries. Closes the gaps where the inline clause was looser
than the schema (e.g. access==NONE with stale userPermissions, GLOBAL_ROLE
without a role, defaultAccessType==GLOBAL_ROLE without auth().role, etc.).

The recursive CTE keeps using raw $queryRaw — it's gated by the enhanced
projects.findUnique, and folder read access is derived from project read
access in schema.zmodel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@therealbrad therealbrad merged commit 563deb4 into main Apr 23, 2026
5 checks passed
@therealbrad therealbrad deleted the fix/show-all-descendants-breaks-with-deep-nested-folders branch April 23, 2026 09:54
@therealbrad
Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.22.8 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant