Skip to content

feat(web): add oxford views prompt management#11

Open
napan24 wants to merge 16 commits into
mainfrom
OxfordViewsIntegration
Open

feat(web): add oxford views prompt management#11
napan24 wants to merge 16 commits into
mainfrom
OxfordViewsIntegration

Conversation

@napan24
Copy link
Copy Markdown
Collaborator

@napan24 napan24 commented May 26, 2026

What does this PR do?

PR title must follow Conventional Commits, for example feat(web): add trace filters or fix: handle empty dataset names.

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (tooling, dependencies, CI, workflows, repo upkeep, or other maintenance work)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (restructures existing code without changing behavior, e.g. simplify logic, split modules, reduce duplication)
  • This change requires a documentation update

Mandatory Tasks

  • Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project (pnpm run format)
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my PR needs changes to the documentation
  • I haven't checked if my changes generate no new warnings (npm run lint)
  • I haven't added tests that prove my fix is effective or that my feature works
  • I haven't checked if new and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Oxford Views: create/edit text & chat prompts with versioning, folders, draft recovery, and form validation
    • Detail page with content/config tabs, version history, label/tag management, duplicate, delete (version/folder), and Python/JS usage snippets
    • Listing/table with search, filters, pagination, breadcrumbs, and sidebar route
    • UI controls: create form, label manager, duplicate dialog, delete confirmations, and version timeline
  • Backend

    • Persistent storage, DB migration, and API endpoints for Oxford Views (create/duplicate/delete/labels/tags/versioning) including public HTTP endpoints with validation and rate limiting

Review Change Stack

napan24 and others added 8 commits May 20, 2026 13:21
Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

Workflow (#4)

* ci: add Docker publish workflow for PRs and main branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): pin action versions to full SHAs to fix zizmor alerts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

Workflow (#5)

* ci: add Docker publish workflow for PRs and main branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): pin action versions to full SHAs to fix zizmor alerts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): fix invalid secrets expressions causing workflow to be silently skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): remove invalid Docker Hub secrets if-condition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): fix incorrect action SHAs for buildx and build-push-action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): use version tags instead of broken SHAs for docker actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docker): fix cross-compilation of migrate binary for arm64

Removes GOBIN so go install works when cross-compiling. The binary lands
in $GOPATH/bin/${GOOS}_${GOARCH}/migrate for cross builds and
$GOPATH/bin/migrate for native builds; copy whichever exists to /out/migrate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docker): fix cross-compilation of migrate binary in devcontainer Dockerfile

Same GOBIN cross-compilation fix as web/Dockerfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

reverted docker files (#7)

* reverted docker files

* only build amd images

---------

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>

fix line issues

Update Dockerfile

Docker revert (#8)

* ci: add Docker publish workflow for PRs and main branch (#2)

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Workflow (#4)

* ci: add Docker publish workflow for PRs and main branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): pin action versions to full SHAs to fix zizmor alerts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Workflow (#5)

* ci: add Docker publish workflow for PRs and main branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): pin action versions to full SHAs to fix zizmor alerts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): fix invalid secrets expressions causing workflow to be silently skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): remove invalid Docker Hub secrets if-condition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): fix incorrect action SHAs for buildx and build-push-action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(docker): use version tags instead of broken SHAs for docker actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docker): fix cross-compilation of migrate binary for arm64

Removes GOBIN so go install works when cross-compiling. The binary lands
in $GOPATH/bin/${GOOS}_${GOARCH}/migrate for cross builds and
$GOPATH/bin/migrate for native builds; copy whichever exists to /out/migrate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docker): fix cross-compilation of migrate binary in devcontainer Dockerfile

Same GOBIN cross-compilation fix as web/Dockerfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* reverted docker files

* only build amd images

* reverted .devcontainer dockerfile

* merge

* Update Dockerfile

* Update Dockerfile

* Update Dockerfile

---------

Co-authored-by: napan.vijayvargiya <napan.vijayvargiya@flipkart.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kinshuk Bairagi <kingster@users.noreply.github.com>
…r/langfuse into OxfordViewsIntegration

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Oxford Views: DB model + migration, server TRPC router and public API endpoints, Zod validation and creation UI, listing/detail pages and management components (labels, duplicate, delete), sidebar route, and a Docker publish GitHub Actions workflow.

Changes

Oxford Views Feature

Layer / File(s) Summary
Database schema and persistence
packages/shared/prisma/migrations/20260526000000_add_oxford_views/migration.sql, packages/shared/prisma/schema.prisma
Prisma migration and schema define oxford_views table/model with versioning, JSON config, tags/labels arrays, FK to projects, composite unique (projectId, name, version), and indexes including a GIN index on tags.
TRPC router and SQL helpers
web/src/features/oxford-views/server/routers/oxfordViewRouter.ts, web/src/server/api/root.ts
oxfordViewRouter implements queries (hasAny, all, count, byId, filterOptions, allLabels, allNames, allVersions) and mutations (create, duplicateView, duplicateFolder, delete, deleteVersion, setLabels, updateTags) with RBAC, CTE-based latest-per-name SQL, transactions, and audit logging; router wired into appRouter.
Public API endpoints
web/src/pages/api/public/oxford-views/index.ts, web/src/pages/api/public/oxford-views/[viewName]/index.ts
Adds authenticated, CORS-enabled GET and POST endpoints for project-scoped Oxford Views and a name-based GET route supporting version/label selection, rate-limiting, validation, transactional label/version invariants, and centralized error mapping.
Form validation and creation UI
web/src/features/oxford-views/components/NewOxfordViewForm/validation.ts, web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx
Zod schema validates the create form and NewOxfordViewForm handles initial data parsing, entries editing, production labeling, commit message, TRPC create mutation, analytics capture, and navigation on success.
Detail, pages, and code snippets
web/src/features/oxford-views/components/oxford-view-detail.tsx, web/src/features/oxford-views/components/oxford-view-new.tsx, web/src/pages/project/[projectId]/oxford-views/[[...folder]].tsx
Detail page fetches versions, selects active version by version/label, renders Content/Config/Use tabs (chat/text/JSON display and variable extraction), and generates Python/JS usage snippets. NewOxfordView wraps the creation form and shows immutability notice for new versions; page module routes list vs detail.
Listing and management operations
web/src/features/oxford-views/components/oxford-views-table.tsx, web/src/features/oxford-views/components/SetOxfordViewVersionLabels/*, web/src/features/oxford-views/components/delete-oxford-view*.tsx, web/src/features/oxford-views/components/duplicate-oxford-view.tsx, web/src/features/oxford-views/components/oxford-view-history.tsx
OxfordViewsTable renders paginated, folder-aware listings with filters and per-row actions. Label popover manages labels and production toggling. Delete components (folder/view/version) and duplicate dialog provide guarded operations with confirmation and RBAC checks. History node renders per-version timeline with diff previews and scroll-to-current behavior.
Sidebar navigation and server wiring
web/src/components/layouts/routes.tsx, web/src/server/api/root.ts, web/src/pages/project/[projectId]/oxford-views/new.tsx
Adds "Oxford Views" route to the sidebar (requires prompts:read), registers oxfordViewRouter in the server appRouter, and re-exports NewOxfordView for the new route.

CI/CD Infrastructure

Layer / File(s) Summary
Docker build and publish workflow
.github/workflows/docker-publish.yml
GitHub Actions workflow builds and pushes GHCR images for web and worker on manual dispatch, pushes to main, tag pushes v*, and PR events. Exports NEXT_PUBLIC_BUILD_ID from the short git SHA, generates metadata/tags, builds linux/amd64 images with per-component cache, and conditionally pushes for PRs vs non-PR runs.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Frontend as NewOxfordViewForm
  participant TRPC as oxfordViewRouter
  participant DB as Database
  Client->>Frontend: submit create form (name, prompt, labels, commitMessage)
  Frontend->>TRPC: oxfordViews.create(projectId, payload)
  TRPC->>TRPC: validate RBAC, compute final labels/tags, determine version
  TRPC->>DB: transactional INSERT new oxford_views, UPDATE prior labels
  DB-->>TRPC: transaction result
  TRPC-->>Frontend: return created view
  Frontend-->>Client: navigate to detail
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"🐰 I hopped through schemas, routes, and views so bright,
Versions and labels lined up just right,
Tables, forms, and code snippets too,
Docker builds hum — the pipeline's new,
A tiny rabbit cheers the feature's light."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(web): add oxford views prompt management' directly and clearly summarizes the main change—introducing Oxford Views functionality for prompt management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch OxfordViewsIntegration

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ox-security
Copy link
Copy Markdown

ox-security Bot commented May 26, 2026

OX Security Logo

OX Security reviewed this pull request — nothing to fix.

No issues found

Branch OxfordViewsIntegrationmain

View scan in OX Security →

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
.github/workflows/docker-publish.yml (2)

77-77: Confirm the intended behavior for internal PR image publishing.

The push condition allows images from internal PRs (same repository) to be published to GHCR, while correctly blocking external fork PRs. This can clutter the registry with ephemeral PR images and may incur storage costs.

Consider whether internal PR images are needed for testing. If not, simplify the condition to:

push: ${{ github.event_name != 'pull_request' }}

If internal PR images are required for integration testing or preview environments, consider adding cleanup automation or using a separate registry namespace for PR images.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-publish.yml at line 77, Decide whether internal PR
images should be published; if not, simplify the CI condition by replacing the
current push expression (push: ${{ github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository }}) with a
stricter check (push: ${{ github.event_name != 'pull_request' }}) to prevent
publishing images for any PRs; if internal PR publishing is required, keep the
existing expression but add follow-up automation (cleanup or separate GHCR
namespace) to avoid registry clutter.

71-84: ⚡ Quick win

Consider adding container image security scanning.

The workflow builds and pushes Docker images without security scanning. Adding a scanning step (e.g., Trivy, Snyk) before push can catch vulnerabilities in dependencies and base images.

🛡️ Example security scanning step

Add this step after the build-and-push step:

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@0.30.0
        with:
          image-ref: ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:sha-${{ env.NEXT_PUBLIC_BUILD_ID }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

Note: This requires the image to be pulled after push, or alternatively scan the local build cache before push using --input instead of image-ref.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-publish.yml around lines 71 - 84, Add a container
security scan step immediately after the existing "Build and push ${{
matrix.component }}" step: invoke aquasecurity/trivy-action@0.30.0 to scan the
built image (image-ref: ghcr.io/${{ github.repository_owner }}/${{
matrix.image_name }}:sha-${{ env.NEXT_PUBLIC_BUILD_ID }}) with format='sarif',
output='trivy-results.sarif' and severity='CRITICAL,HIGH', then upload the SARIF
with github/codeql-action/upload-sarif@v3 (if: always()) to surface results in
the Security tab; alternatively, if you prefer scanning the local build before
push, configure the Trivy action to scan the local image/cache via --input
instead of image-ref.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/docker-publish.yml:
- Line 72: Replace the floating tag for the Docker build/push action so it's
pinned to a specific commit SHA rather than `@v6`; locate the line using the
action identifier `docker/build-push-action@v6` and update it to the same
SHA-pinning pattern used for other actions (e.g., the pinned SHAs for
checkout/login/metadata) to ensure supply-chain consistency and immutability.
- Around line 39-44: The workflow uses mutable tags for the actions
docker/setup-qemu-action@v3 and docker/setup-buildx-action@v3; update those two
steps to pin the actions to specific commit SHAs (the same pattern used for
checkout, login, and metadata) by replacing the `@v3` tag with the corresponding
full commit SHA for each action so the workflow consistently uses immutable
references.

In `@web/src/features/oxford-views/components/delete-oxford-view-version.tsx`:
- Around line 55-56: The Popover's onOpenChange currently toggles state via
setIsOpen(!isOpen), which can race; change the handler to accept the callback's
open argument and set state directly (e.g., onOpenChange={(open) =>
setIsOpen(open)}), updating the usage in the component where Popover, setIsOpen
and isOpen are defined so the open state is bound from the callback input
instead of inverted.

In `@web/src/features/oxford-views/components/delete-oxford-view.tsx`:
- Line 30: The Popover onOpenChange handler currently toggles local state
(onOpenChange={() => setIsOpen(!isOpen)}) which can desynchronize state; update
the handler to accept the provided open argument and call setIsOpen(open)
instead (i.e., use the onOpenChange open parameter from the Popover component to
set isOpen). Locate the Popover component in delete-oxford-view.tsx and replace
the toggle logic with a handler that uses the open parameter to update isOpen
via setIsOpen(open).

In `@web/src/features/oxford-views/components/oxford-view-detail.tsx`:
- Around line 199-207: The Button currently nests a Link which creates two
interactive elements; change this to render the Link as the Button's child via
the Button's asChild prop: remove the direct interactive nesting by adding
asChild to the Button component and keep the Link as the only interactive
element (preserve
href={`/project/${projectId}/oxford-views/new?promptId=${encodeURIComponent(view.id)}`}
and inner contents including <Plus /> and the span), moving any necessary
styling/className onto the Link so Button props like className don't create a
separate interactive element; update the Button/Link composition around the
Button and Link symbols accordingly.
- Around line 125-137: The memo for pythonCode/jsCode only depends on view?.id
so edits to view.name, view.version or view.labels can leave snippets stale;
update the useMemo dependency list to include view.name, view.version and
view.labels (or a stable serialization of labels) so getPythonCode/getJsCode are
recomputed when those change, and remove the eslint-disable-next-line
react-hooks/exhaustive-deps comment (or adjust it) to reflect the correct
dependencies.

In `@web/src/features/oxford-views/components/oxford-view-history.tsx`:
- Around line 26-38: The useEffect in the OxfordViewHistory component currently
only depends on currentRef.current so it doesn't re-run when the selected
version changes; update the dependency array to include props.currentVersion and
view.version (in addition to currentRef.current) so the effect re-evaluates and
scrolls the newly active item into view, keeping the same scrollIntoView logic
inside the effect.

In `@web/src/features/oxford-views/components/oxford-views-table.tsx`:
- Around line 151-160: The filterOptions query is being called with projectId
possibly equal to an empty string (projectId is set via useProjectIdFromURL() ??
""), and sessionFilterContextId is being set to "" instead of null; guard the
api.oxfordViews.filterOptions.useQuery call so it only runs when projectId is
non-empty (e.g., skip or pass undefined/null until a real id exists) and
normalize sessionFilterContextId to null when projectId is an empty string so
session storage uses the global scope; update references to projectId,
filterOptions.useQuery, useProjectIdFromURL, and sessionFilterContextId
accordingly to prevent querying with "" and to ensure null is used for global
session scoping.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts`:
- Around line 727-737: The code escapes pathPrefix into escapedPathPrefix and
then uses that in the Prisma startsWith filter, which causes literal-mismatch
for folder names containing SQL wildcard characters; change the logic in the
oxfordView.findMany where clause to use the original pathPrefix (not
escapeSqlLikePattern) for the startsWith comparison (i.e. use pathPrefix ?
`${pathPrefix}/` : undefined or similar) and only apply escapeSqlLikePattern
where a SQL LIKE pattern is actually required elsewhere; update references to
escapedPathPrefix accordingly and keep promptName handling the same.
- Around line 570-586: The code wrongly uses escapeSqlLikePattern() for Prisma
startsWith filters which prevents matching names containing '%' or '_' — replace
escapedTarget and escapedSource usages in the two
ctx.prisma.oxfordView.findFirst and findMany calls with the raw targetPath and
sourcePath (preserving the appended "/" as before, e.g. startsWith:
`${targetPath}/` and startsWith: `${sourcePath}/`) so Prisma's startsWith does
real prefix matching; keep escapeSqlLikePattern only for places that build raw
SQL LIKE clauses.
- Around line 669-693: The filterOptions resolver currently queries project data
without enforcing project RBAC; before running the Promise.all, call the
existing project access guard (throwIfNoProjectAccess) using the current context
and input.projectId to validate the caller has access to that project (e.g.,
await ctx.project.throwIfNoProjectAccess(input.projectId)); then proceed with
the three ctx.prisma queries (oxfordView.groupBy and the two
ctx.prisma.$queryRaw calls) so only authorized callers can enumerate names,
tags, and labels.

---

Nitpick comments:
In @.github/workflows/docker-publish.yml:
- Line 77: Decide whether internal PR images should be published; if not,
simplify the CI condition by replacing the current push expression (push: ${{
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository }}) with a
stricter check (push: ${{ github.event_name != 'pull_request' }}) to prevent
publishing images for any PRs; if internal PR publishing is required, keep the
existing expression but add follow-up automation (cleanup or separate GHCR
namespace) to avoid registry clutter.
- Around line 71-84: Add a container security scan step immediately after the
existing "Build and push ${{ matrix.component }}" step: invoke
aquasecurity/trivy-action@0.30.0 to scan the built image (image-ref: ghcr.io/${{
github.repository_owner }}/${{ matrix.image_name }}:sha-${{
env.NEXT_PUBLIC_BUILD_ID }}) with format='sarif', output='trivy-results.sarif'
and severity='CRITICAL,HIGH', then upload the SARIF with
github/codeql-action/upload-sarif@v3 (if: always()) to surface results in the
Security tab; alternatively, if you prefer scanning the local build before push,
configure the Trivy action to scan the local image/cache via --input instead of
image-ref.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddc88987-bff6-45f9-adc4-2175651258a9

📥 Commits

Reviewing files that changed from the base of the PR and between ebf32c7 and 4470117.

📒 Files selected for processing (20)
  • .github/workflows/docker-publish.yml
  • packages/shared/prisma/migrations/20260526000000_add_oxford_views/migration.sql
  • packages/shared/prisma/schema.prisma
  • web/src/components/layouts/routes.tsx
  • web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx
  • web/src/features/oxford-views/components/NewOxfordViewForm/validation.ts
  • web/src/features/oxford-views/components/SetOxfordViewVersionLabels/LabelCommandItem.tsx
  • web/src/features/oxford-views/components/SetOxfordViewVersionLabels/index.tsx
  • web/src/features/oxford-views/components/delete-oxford-view-folder.tsx
  • web/src/features/oxford-views/components/delete-oxford-view-version.tsx
  • web/src/features/oxford-views/components/delete-oxford-view.tsx
  • web/src/features/oxford-views/components/duplicate-oxford-view.tsx
  • web/src/features/oxford-views/components/oxford-view-detail.tsx
  • web/src/features/oxford-views/components/oxford-view-history.tsx
  • web/src/features/oxford-views/components/oxford-view-new.tsx
  • web/src/features/oxford-views/components/oxford-views-table.tsx
  • web/src/features/oxford-views/server/routers/oxfordViewRouter.ts
  • web/src/pages/project/[projectId]/oxford-views/[[...folder]].tsx
  • web/src/pages/project/[projectId]/oxford-views/new.tsx
  • web/src/server/api/root.ts

Comment on lines +39 to +44
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Pin QEMU and Buildx action versions to commit SHAs for consistency.

Lines 40 and 43 use version tags (@v3), while other actions in this workflow (checkout at line 35, login at line 46, metadata at line 57) are SHA-pinned. This inconsistency weakens supply chain security, as version tags are mutable and can be updated by action maintainers.

🔒 Suggested fix to pin action versions
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3
+        uses: docker/setup-qemu-action@49b3bc8e1090540494070c6f67e47985cd5a6314 # v3

       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
+        uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@49b3bc8e1090540494070c6f67e47985cd5a6314 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-publish.yml around lines 39 - 44, The workflow uses
mutable tags for the actions docker/setup-qemu-action@v3 and
docker/setup-buildx-action@v3; update those two steps to pin the actions to
specific commit SHAs (the same pattern used for checkout, login, and metadata)
by replacing the `@v3` tag with the corresponding full commit SHA for each action
so the workflow consistently uses immutable references.

type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

- name: Build and push ${{ matrix.component }}
uses: docker/build-push-action@v6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Pin build-push-action to commit SHA for consistency.

Line 72 uses a version tag (@v6), while other actions like checkout (line 35), login (line 46), and metadata (line 57) are SHA-pinned. See earlier comment on lines 39-44 for the same supply chain security concern.

🔒 Suggested fix
       - name: Build and push ${{ matrix.component }}
-        uses: docker/build-push-action@v6
+        uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v6
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: docker/build-push-action@v6
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-publish.yml at line 72, Replace the floating tag
for the Docker build/push action so it's pinned to a specific commit SHA rather
than `@v6`; locate the line using the action identifier
`docker/build-push-action@v6` and update it to the same SHA-pinning pattern used
for other actions (e.g., the pinned SHAs for checkout/login/metadata) to ensure
supply-chain consistency and immutability.

Comment on lines +55 to +56
onOpenChange={() => setIsOpen(!isOpen)}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bind Popover open state directly from callback input.

Line 55 should use the open parameter; toggling with !isOpen can flip to the wrong state on repeated callback invocations.

Suggested fix
-      onOpenChange={() => setIsOpen(!isOpen)}
+      onOpenChange={setIsOpen}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onOpenChange={() => setIsOpen(!isOpen)}
>
onOpenChange={setIsOpen}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/delete-oxford-view-version.tsx`
around lines 55 - 56, The Popover's onOpenChange currently toggles state via
setIsOpen(!isOpen), which can race; change the handler to accept the callback's
open argument and set state directly (e.g., onOpenChange={(open) =>
setIsOpen(open)}), updating the usage in the component where Popover, setIsOpen
and isOpen are defined so the open state is bound from the callback input
instead of inverted.

});

return (
<Popover open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the open argument from onOpenChange instead of toggling local state.

Line 30 can desynchronize the popover state when the callback fires multiple times with the same target value.

Suggested fix
-    <Popover open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
+    <Popover open={isOpen} onOpenChange={setIsOpen}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Popover open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<Popover open={isOpen} onOpenChange={setIsOpen}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/delete-oxford-view.tsx` at line 30,
The Popover onOpenChange handler currently toggles local state (onOpenChange={()
=> setIsOpen(!isOpen)}) which can desynchronize state; update the handler to
accept the provided open argument and call setIsOpen(open) instead (i.e., use
the onOpenChange open parameter from the Popover component to set isOpen).
Locate the Popover component in delete-oxford-view.tsx and replace the toggle
logic with a handler that uses the open parameter to update isOpen via
setIsOpen(open).

Comment on lines +125 to +137
const { pythonCode, jsCode } = useMemo(() => {
if (!view?.id) return { pythonCode: null, jsCode: null };
const sortedLabels = [...view.labels].sort((a, b) => {
if (a === PRODUCTION_LABEL) return -1;
if (b === PRODUCTION_LABEL) return 1;
return a.localeCompare(b);
});
return {
pythonCode: getPythonCode(view.name, view.version, sortedLabels),
jsCode: getJsCode(view.name, view.version, sortedLabels),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [view?.id]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant snippet and surrounding lines
FILE="web/src/features/oxford-views/components/oxford-view-detail.tsx"
nl -ba "$FILE" | sed -n '90,170p'

# Find where view is sourced/updated in this component
rg -n "const \\{ ?pythonCode, jsCode \\} = useMemo\\(" "$FILE"
rg -n "view\\?" "$FILE"
rg -n "setView\\b|on.*label|labels" "$FILE"

# Locate helpers used by the memo (getPythonCode/getJsCode) to see inputs/expectations
rg -n "function getPythonCode|getPythonCode\\b|getJsCode\\b" "$FILE" || true
rg -n "getPythonCode\\b|getJsCode\\b" "$FILE"

# Search other components that edit labels for an Oxford view and how they update state
rg -n "oxford-views|OxfordView|view\\.labels|PRODUCTION_LABEL" web/src/features/oxford-views -S

Repository: flipkart-incubator/langfuse

Length of output: 114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="web/src/features/oxford-views/components/oxford-view-detail.tsx"

# Show relevant section around the memo with line numbers
cat -n "$FILE" | sed -n '90,170p'

# Find the memo and its dependency array
rg -n "const \\{ *pythonCode, *jsCode *\\} *= *useMemo\\(" "$FILE"
rg -n "useMemo\\(" "$FILE" || true
rg -n "}, *\\[view\\?\\.id\\]" "$FILE" || true

# Check all references to view labels/name/version in this component
rg -n "view\\?(\\.labels|\\.name|\\.version)" "$FILE" || true
rg -n "view\\.(labels|name|version)" "$FILE" || true

# Look for state updates: setView / onChange handlers that might mutate view labels/version
rg -n "\\bsetView\\b|on.*label|labels.*on|set.*labels|handle.*label|PRODUCTION_LABEL" "$FILE"

# Find where view is fetched/constructed in this component
rg -n "use.*(query|fetch|get)\\b|view\\s*=|const \\s*view\\b" "$FILE" || true

# Locate helper signatures used by the memo
rg -n "getPythonCode\\b|getJsCode\\b" "$FILE"

Repository: flipkart-incubator/langfuse

Length of output: 5089


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="web/src/features/oxford-views/components/oxford-view-detail.tsx"

# Show the rest of the component around the tabs/actions and any label editing handlers
cat -n "$FILE" | sed -n '170,330p'

# Search for mutations / refetches / cache updates related to prompt labels or versions
rg -n "promptLabels|labels\\s*=|on.*Label|label.*onChange|update.*label|mutate.*label|refetch|invalidate|oxfordViews|promptVersions" "$FILE"

# Search for API calls that likely update versions/labels
rg -n "api\\.(oxfordViews|prompt).*\\.(update|mutate|create|delete)" "$FILE" || true

# Search for components that handle label editing and their props
rg -n "Label|popover|popover|Use View|New version|version history" "$FILE"

Repository: flipkart-incubator/langfuse

Length of output: 10112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the label editing component that’s passed into oxford-view-detail.tsx
FILE="web/src/features/oxford-views/components/SetOxfordViewVersionLabels.tsx"

test -f "$FILE" && echo "Found: $FILE" || (echo "Missing: $FILE"; exit 1)

cat -n "$FILE" | sed -n '1,260p'

# Find how it updates labels/version and whether it triggers refetch/invalidation
rg -n "invalidate|refetch|mutate|update|setQuery|onSuccess|onError|labels|promptLabels|view\\.|version\\b" "$FILE"

Repository: flipkart-incubator/langfuse

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the file that exports SetOxfordViewVersionLabels
rg -n "export (function|const|default) SetOxfordViewVersionLabels" web/src/features/oxford-views -S || true
rg -n "SetOxfordViewVersionLabels" web/src/features/oxford-views -S

# Also try filename-based discovery
fd "SetOxfordViewVersionLabels" web/src/features/oxford-views -a || true
fd "*SetOxfordViewVersionLabels*" web/src/features/oxford-views -a || true

Repository: flipkart-incubator/langfuse

Length of output: 1317


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="web/src/features/oxford-views/components/SetOxfordViewVersionLabels/index.tsx"

cat -n "$FILE" | sed -n '1,260p'

rg -n "invalidate|refetch|mutate|onSuccess|onError|setQuery|router|push|replace|setCurrent|currentLabel|label\\b|promptLabels|version\\b" "$FILE"

Repository: flipkart-incubator/langfuse

Length of output: 12025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the Button component file and inspect its rendered element
rg -n "export \\{.*Button\\}" web/src/components/ui/button.tsx web/src/components/ui/button.* web/src/components/ui -S || true
fd "button" web/src/components/ui -a || true

# Try common locations
for f in \
  web/src/components/ui/button.ts \
  web/src/components/ui/button.tsx \
  web/src/components/ui/Button.tsx \
  web/src/components/ui/Button.ts \
; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    cat -n "$f" | head -n 200
  fi
done

# Also inspect where next/link is used and what it renders (if this repo wraps it)
rg -n "from ['\"]next/link['\"]" web/src -S | head -n 50

Repository: flipkart-incubator/langfuse

Length of output: 8923


Recompute SDK “Use View” snippets when the selected view’s labels/name/version change
The memo only keys off view?.id, so label edits for the currently selected version (the mutation invalidates oxfordViews.allVersions) can update view.labels/view.name/view.version while keeping view.id, leaving pythonCode/jsCode stale.

Suggested fix
   const { pythonCode, jsCode } = useMemo(() => {
     if (!view?.id) return { pythonCode: null, jsCode: null };
@@
     return {
       pythonCode: getPythonCode(view.name, view.version, sortedLabels),
       jsCode: getJsCode(view.name, view.version, sortedLabels),
     };
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [view?.id]);
+  }, [view?.id, view?.name, view?.version, view?.labels]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { pythonCode, jsCode } = useMemo(() => {
if (!view?.id) return { pythonCode: null, jsCode: null };
const sortedLabels = [...view.labels].sort((a, b) => {
if (a === PRODUCTION_LABEL) return -1;
if (b === PRODUCTION_LABEL) return 1;
return a.localeCompare(b);
});
return {
pythonCode: getPythonCode(view.name, view.version, sortedLabels),
jsCode: getJsCode(view.name, view.version, sortedLabels),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [view?.id]);
const { pythonCode, jsCode } = useMemo(() => {
if (!view?.id) return { pythonCode: null, jsCode: null };
const sortedLabels = [...view.labels].sort((a, b) => {
if (a === PRODUCTION_LABEL) return -1;
if (b === PRODUCTION_LABEL) return 1;
return a.localeCompare(b);
});
return {
pythonCode: getPythonCode(view.name, view.version, sortedLabels),
jsCode: getJsCode(view.name, view.version, sortedLabels),
};
}, [view?.id, view?.name, view?.version, view?.labels]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/oxford-view-detail.tsx` around lines
125 - 137, The memo for pythonCode/jsCode only depends on view?.id so edits to
view.name, view.version or view.labels can leave snippets stale; update the
useMemo dependency list to include view.name, view.version and view.labels (or a
stable serialization of labels) so getPythonCode/getJsCode are recomputed when
those change, and remove the eslint-disable-next-line
react-hooks/exhaustive-deps comment (or adjust it) to reflect the correct
dependencies.

Comment on lines +26 to +38
useEffect(() => {
if (
props.currentVersion &&
currentRef.current &&
props.currentVersion === view.version
) {
currentRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentRef.current]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="web/src/features/oxford-views/components/oxford-view-history.tsx"
if [ ! -f "$file" ]; then
  echo "Missing file: $file"
  exit 1
fi
echo "=== File: $file ==="
# Show around the useEffect with dependency array
rg -n "useEffect\(\s*\(\)\s*=>|useEffect\(" -n "$file"
# Print the specific region near line 26-60 and near the useEffect dependency line
nl -ba "$file" | sed -n '1,140p'
nl -ba "$file" | sed -n '140,260p'

Repository: flipkart-incubator/langfuse

Length of output: 235


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="web/src/features/oxford-views/components/oxford-view-history.tsx"
if [ ! -f "$file" ]; then
  echo "Missing file: $file"
  exit 1
fi

echo "=== useEffect regions ==="
# Show the specific lines around the effect and dependency array
cat -n "$file" | sed -n '1,120p'
echo "=== tail around effect ==="
cat -n "$file" | sed -n '120,240p'

echo "=== extract exact useEffect block ==="
# Print from useEffect line to the dependency array line (best-effort)
start_line=$(rg -n "useEffect\(\s*\(\)\s*=>" "$file" | head -n1 | cut -d: -f1)
echo "Detected useEffect start line: $start_line"
if [ -n "${start_line:-}" ]; then
  end_line=$((start_line+60))
  cat -n "$file" | sed -n "${start_line},${end_line}p"
fi

Repository: flipkart-incubator/langfuse

Length of output: 9176


Fix auto-scroll to rerun when the selected version changes

The effect depends on currentRef.current, which won’t change when props.currentVersion updates, so it may not scroll the newly active item into view. Include props.currentVersion (and view.version) in the dependency list.

Suggested fix
   useEffect(() => {
     if (
       props.currentVersion &&
       currentRef.current &&
       props.currentVersion === view.version
@@
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [currentRef.current]);
+  }, [props.currentVersion, view.version]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (
props.currentVersion &&
currentRef.current &&
props.currentVersion === view.version
) {
currentRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentRef.current]);
useEffect(() => {
if (
props.currentVersion &&
currentRef.current &&
props.currentVersion === view.version
) {
currentRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, [props.currentVersion, view.version]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/oxford-view-history.tsx` around
lines 26 - 38, The useEffect in the OxfordViewHistory component currently only
depends on currentRef.current so it doesn't re-run when the selected version
changes; update the dependency array to include props.currentVersion and
view.version (in addition to currentRef.current) so the effect re-evaluates and
scrolls the newly active item into view, keeping the same scrollIntoView logic
inside the effect.

Comment on lines +151 to +160
const filterOptions = api.oxfordViews.filterOptions.useQuery(
{ projectId },
{
trpc: { context: { skipBatch: true } },
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Infinity,
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== filterOptions definition and projectId validation =="
rg -n -C4 'filterOptions|projectId' web/src/features/oxford-views/server/routers/oxfordViewRouter.ts

echo
echo "== caller usage in oxford-views-table =="
rg -n -C4 'api\.oxfordViews\.filterOptions\.useQuery|enabled:|sessionFilterContextId' web/src/features/oxford-views/components/oxford-views-table.tsx

Repository: flipkart-incubator/langfuse

Length of output: 18515


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== oxford-views-table.tsx: projectId initialization and surrounding lines =="
sed -n '120,210p' web/src/features/oxford-views/components/oxford-views-table.tsx

echo
echo "== locate useSidebarFilterState implementation =="
rg -n "function useSidebarFilterState|useSidebarFilterState\\(" web/src -S

# Try to show the most relevant implementation file
impl_file="$(rg -n "useSidebarFilterState\\(" web/src -S --files-with-matches | head -n 1 || true)"
if [[ -n "${impl_file:-}" ]]; then
  echo
  echo "== candidate implementation file: $impl_file =="
  wc -l "$impl_file"
  sed -n '1,220p' "$impl_file"
fi

Repository: flipkart-incubator/langfuse

Length of output: 11842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useSidebarFilterState.tsx: sessionFilterContextId usage =="
rg -n "sessionFilterContextId" web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '380,520p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Also show any helpers building sessionStorage keys =="
rg -n "sessionStorage|sessionFilter|storage.*key|filterContextId" web/src/features/filters/hooks/useSidebarFilterState.tsx
rg -n "sessionFilterContextId" web/src/features/filters -S

echo
echo "== useProjectIdFromURL initial value behavior =="
rg -n "function useProjectIdFromURL|export default function useProjectIdFromURL" web/src -S
proj_hook="$(rg -n "useProjectIdFromURL" web/src -S --files-with-matches | head -n 5 | sed -n '1,1p')"
echo "candidate hook file: $proj_hook"
if [[ -n "$proj_hook" ]]; then
  sed -n '1,220p' "$proj_hook"
fi

echo
echo "== throwIfNoProjectAccess behavior for empty projectId =="
rg -n "function throwIfNoProjectAccess|throwIfNoProjectAccess\\(" web/src -S
throw_file="$(rg -n "throwIfNoProjectAccess\\(" web/src -S --files-with-matches | head -n 1)"
echo "candidate throwIfNoProjectAccess file: $throw_file"
if [[ -n "$throw_file" ]]; then
  rg -n "throwIfNoProjectAccess" "$throw_file"
  sed -n '1,240p' "$throw_file"
fi

Repository: flipkart-incubator/langfuse

Length of output: 38892


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useProjectIdFromURL.ts =="
sed -n '1,200p' web/src/hooks/useProjectIdFromURL.ts

echo
echo "== buildSidebarFilterQueryStorageKey definition + createPersistedSidebarFilterQueryState =="
rg -n "buildSidebarFilterQueryStorageKey|createPersistedSidebarFilterQueryState|getPersistedSidebarFilterQueryForContext" web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '1,220p' web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '220,520p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Look for effects that synchronize memoryFilterState when context changes =="
rg -n "useEffect\\(|storedFiltersQuery|normalizedSessionFilterContextId|FILTER_QUERY_SESSION_STORAGE_KEY|setMemoryFilterState" web/src/features/filters/hooks/useSidebarFilterState.tsx

# Show likely sections around storedFiltersQuery / memoryFilterState updates
sed -n '520,820p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Relevant tests for sidebar filter session persistence =="
sed -n '1,220p' web/src/features/filters/sidebarFilterSessionPersistence.clienttest.tsx

echo
echo "== Find how other tables pass sessionFilterContextId =="
rg -n "sessionFilterContextId:\\s*projectId" web/src/features web/src/components -S

Repository: flipkart-incubator/langfuse

Length of output: 35250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useProjectIdFromURL.ts =="
sed -n '1,200p' web/src/hooks/useProjectIdFromURL.ts

echo
echo "== buildSidebarFilterQueryStorageKey definition + createPersistedSidebarFilterQueryState =="
rg -n "buildSidebarFilterQueryStorageKey|createPersistedSidebarFilterQueryState|getPersistedSidebarFilterQueryForContext" web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '1,220p' web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '220,520p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Look for effects that synchronize memoryFilterState when context changes =="
rg -n "useEffect\\(|storedFiltersQuery|normalizedSessionFilterContextId|FILTER_QUERY_SESSION_STORAGE_KEY|setMemoryFilterState" web/src/features/filters/hooks/useSidebarFilterState.tsx

# Show likely sections around storedFiltersQuery / memoryFilterState updates
sed -n '520,820p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Relevant tests for sidebar filter session persistence =="
sed -n '1,240p' web/src/features/filters/sidebarFilterSessionPersistence.clienttest.tsx

echo
echo "== Find how other tables pass sessionFilterContextId =="
rg -n "sessionFilterContextId:\\s*projectId" web/src/features web/src/components -S

Repository: flipkart-incubator/langfuse

Length of output: 35784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useProjectIdFromURL.ts =="
sed -n '1,200p' web/src/hooks/useProjectIdFromURL.ts

echo
echo "== buildSidebarFilterQueryStorageKey / persisted state helpers =="
rg -n "buildSidebarFilterQueryStorageKey|createPersistedSidebarFilterQueryState|getPersistedSidebarFilterQueryForContext" web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '1,260p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Keyed session storage + storedFiltersQuery selection =="
sed -n '260,520p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Sync logic for memoryFilterState when context changes =="
rg -n "useEffect\\(|storedFiltersQuery|normalizedSessionFilterContextId|setMemoryFilterState" web/src/features/filters/hooks/useSidebarFilterState.tsx
sed -n '520,900p' web/src/features/filters/hooks/useSidebarFilterState.tsx

echo
echo "== Tests that cover sidebarFilterSessionPersistence =="
sed -n '1,260p' web/src/features/filters/sidebarFilterSessionPersistence.clienttest.tsx

echo
echo "== Other call sites for sessionFilterContextId =="
rg -n "sessionFilterContextId:\\s*[^,\\}]*projectId" web/src/features web/src/components -S

Repository: flipkart-incubator/langfuse

Length of output: 38893


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== persistedSidebarFilterQuery.ts: storage key behavior for null/empty contextId =="
rg -n "function buildSidebarFilterQueryStorageKey|buildSidebarFilterQueryStorageKey" web/src/features/filters/lib -S
file="$(rg -n "buildSidebarFilterQueryStorageKey\\(" web/src/features/filters/lib -S --files-with-matches | head -n 1)"
echo "file: $file"
sed -n '1,220p' "$file"

echo
echo "== getPersistedSidebarFilterQueryForContext: context comparison logic =="
rg -n "getPersistedSidebarFilterQueryForContext" web/src/features/filters/lib -S
sed -n '220,420p' "$file"

Repository: flipkart-incubator/langfuse

Length of output: 2678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== throwIfNoProjectAccess implementation =="
rg -n "export function throwIfNoProjectAccess|function throwIfNoProjectAccess" web/src/features/rbac/utils -S
file="$(rg -n "export function throwIfNoProjectAccess|function throwIfNoProjectAccess" web/src/features/rbac/utils -S --files-with-matches | head -n 1 || true)"
echo "file: $file"
if [[ -n "$file" ]]; then
  sed -n '1,240p' "$file"
fi

echo
echo "== check for any explicit empty-string handling =="
if [[ -n "$file" ]]; then
  rg -n "''|empty|length|projectId\\.length|trim\\(" "$file" || true
fi

Repository: flipkart-incubator/langfuse

Length of output: 115


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== find throwIfNoProjectAccess definition in web/src =="
rg -n "throwIfNoProjectAccess\\b" web/src -S | head -n 50

echo
echo "== candidate definition files =="
rg -n "export function throwIfNoProjectAccess\\b|function throwIfNoProjectAccess\\b" web/src -S --files-with-matches

echo
file="$(rg -n "export function throwIfNoProjectAccess\\b|function throwIfNoProjectAccess\\b" web/src -S --files-with-matches | head -n 1 || true)"
echo "first match file: $file"
if [[ -n "${file:-}" ]]; then
  sed -n '1,240p' "$file"
fi

Repository: flipkart-incubator/langfuse

Length of output: 4790


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' web/src/features/rbac/utils/checkProjectAccess.ts

Repository: flipkart-incubator/langfuse

Length of output: 2131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' web/src/features/filters/hooks/useKeyedSessionStorageState.ts

Repository: flipkart-incubator/langfuse

Length of output: 2540


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "contextId=\"|contextId: \"|\\\"\\\"" web/src/features/filters/sidebarFilterSessionPersistence.clienttest.tsx web/src/features/filters -S

Repository: flipkart-incubator/langfuse

Length of output: 192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== oxfordViewRouter filterOptions query body (for access checks) =="
sed -n '640,730p' web/src/features/oxford-views/server/routers/oxfordViewRouter.ts

echo
echo "== useKeyedSessionStorageState implementation =="
sed -n '1,240p' web/src/features/filters/hooks/useKeyedSessionStorageState.ts

echo
echo "== any tests using empty-string contextId in sidebar filter persistence =="
rg -n "contextId\\s*[:=]\\s*(\"\"|null|undefined)|\"\"\\s*\\)|contextId\\s*=\\s*\"\"" web/src/features/filters/sidebarFilterSessionPersistence.clienttest.tsx web/src/features/filters -S

Repository: flipkart-incubator/langfuse

Length of output: 7949


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== oxfordViewRouter filterOptions query body (for access checks) =="
sed -n '640,730p' web/src/features/oxford-views/server/routers/oxfordViewRouter.ts

echo
echo "== useKeyedSessionStorageState implementation =="
sed -n '1,240p' web/src/features/filters/hooks/useKeyedSessionStorageState.ts

echo
echo "== tests using empty-string contextId in sidebar filter persistence =="
rg -n "contextId\\s*[:=]\\s*(\"\"|null|undefined)|\"\"\\s*\\)|contextId\\s*=\\s*\"\"" web/src/features/filters/sidebarFilterSessionPersistence.clienttest.tsx web/src/features/filters -S

Repository: flipkart-incubator/langfuse

Length of output: 7945


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== other usages of filterOptions.useQuery with enabled guard =="
rg -n "filterOptions\\.useQuery\\(" web/src/features -S --glob='*.tsx' --glob='*.ts' | head -n 50

echo
echo "== filterOptions.useQuery blocks that contain enabled: Boolean(projectId) =="
rg -n "filterOptions\\.useQuery\\([\\s\\S]{0,300}?enabled\\s*:\\s*Boolean\\(projectId\\)" web/src/features -S --glob='*.tsx' --glob='*.ts' || true

echo
echo "== filterOptions.useQuery blocks with sessionFilterContextId/projectId empty handling =="
rg -n "filterOptions\\.useQuery\\([\\s\\S]{0,120}projectId" web/src/features -S --glob='*.tsx' | head -n 50

Repository: flipkart-incubator/langfuse

Length of output: 1768


Guard filterOptions query + normalize sessionFilterContextId when projectId is empty

oxford-views-table.tsx initializes projectId as useProjectIdFromURL() ?? "" and filterOptions.useQuery is currently unguarded, so it can call the filterOptions tRPC procedure with projectId="" before the URL param is ready (the router accepts empty strings via z.string(), then rejects via access checks). Also, sessionFilterContextId: projectId ?? null preserves "", creating a distinct sessionStorage scope instead of falling back to the intended null/global behavior.

Suggested patch
   const filterOptions = api.oxfordViews.filterOptions.useQuery(
     { projectId },
     {
+      enabled: Boolean(projectId),
       trpc: { context: { skipBatch: true } },
       refetchOnMount: false,
       refetchOnWindowFocus: false,
       refetchOnReconnect: false,
       staleTime: Infinity,
     },
   );
@@
   const queryFilter = useSidebarFilterState(
@@
     {
       loading: filterOptions.isPending,
       stateLocation: "urlAndSessionStorage",
-      sessionFilterContextId: projectId ?? null,
+      sessionFilterContextId: projectId || null,
     },
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/oxford-views-table.tsx` around lines
151 - 160, The filterOptions query is being called with projectId possibly equal
to an empty string (projectId is set via useProjectIdFromURL() ?? ""), and
sessionFilterContextId is being set to "" instead of null; guard the
api.oxfordViews.filterOptions.useQuery call so it only runs when projectId is
non-empty (e.g., skip or pass undefined/null until a real id exists) and
normalize sessionFilterContextId to null when projectId is an empty string so
session storage uses the global scope; update references to projectId,
filterOptions.useQuery, useProjectIdFromURL, and sessionFilterContextId
accordingly to prevent querying with "" and to ensure null is used for global
session scoping.

Comment on lines +570 to +586
const { projectId, sourcePath, targetPath, isSingleVersion } = input;
const escapedTarget = escapeSqlLikePattern(targetPath);
const escapedSource = escapeSqlLikePattern(sourcePath);

const existingTarget = await ctx.prisma.oxfordView.findFirst({
where: { projectId, name: { startsWith: `${escapedTarget}/` } },
});
if (existingTarget) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Oxford Views already exist under "${targetPath}/".`,
});
}

const sourceViews = await ctx.prisma.oxfordView.findMany({
where: { projectId, name: { startsWith: `${escapedSource}/` } },
orderBy: [{ name: "asc" }, { version: "asc" }],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use raw folder paths for Prisma startsWith filters.

escapeSqlLikePattern() is only correct for raw LIKE clauses. Using the escaped value in startsWith makes names containing % or _ stop matching, so duplicate-folder can falsely report missing source folders or clear target paths.

Suggested fix
       const { projectId, sourcePath, targetPath, isSingleVersion } = input;
-      const escapedTarget = escapeSqlLikePattern(targetPath);
-      const escapedSource = escapeSqlLikePattern(sourcePath);
 
       const existingTarget = await ctx.prisma.oxfordView.findFirst({
-        where: { projectId, name: { startsWith: `${escapedTarget}/` } },
+        where: { projectId, name: { startsWith: `${targetPath}/` } },
       });
@@
       const sourceViews = await ctx.prisma.oxfordView.findMany({
-        where: { projectId, name: { startsWith: `${escapedSource}/` } },
+        where: { projectId, name: { startsWith: `${sourcePath}/` } },
         orderBy: [{ name: "asc" }, { version: "asc" }],
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { projectId, sourcePath, targetPath, isSingleVersion } = input;
const escapedTarget = escapeSqlLikePattern(targetPath);
const escapedSource = escapeSqlLikePattern(sourcePath);
const existingTarget = await ctx.prisma.oxfordView.findFirst({
where: { projectId, name: { startsWith: `${escapedTarget}/` } },
});
if (existingTarget) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Oxford Views already exist under "${targetPath}/".`,
});
}
const sourceViews = await ctx.prisma.oxfordView.findMany({
where: { projectId, name: { startsWith: `${escapedSource}/` } },
orderBy: [{ name: "asc" }, { version: "asc" }],
const { projectId, sourcePath, targetPath, isSingleVersion } = input;
const existingTarget = await ctx.prisma.oxfordView.findFirst({
where: { projectId, name: { startsWith: `${targetPath}/` } },
});
if (existingTarget) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Oxford Views already exist under "${targetPath}/".`,
});
}
const sourceViews = await ctx.prisma.oxfordView.findMany({
where: { projectId, name: { startsWith: `${sourcePath}/` } },
orderBy: [{ name: "asc" }, { version: "asc" }],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` around
lines 570 - 586, The code wrongly uses escapeSqlLikePattern() for Prisma
startsWith filters which prevents matching names containing '%' or '_' — replace
escapedTarget and escapedSource usages in the two
ctx.prisma.oxfordView.findFirst and findMany calls with the raw targetPath and
sourcePath (preserving the appended "/" as before, e.g. startsWith:
`${targetPath}/` and startsWith: `${sourcePath}/`) so Prisma's startsWith does
real prefix matching; keep escapeSqlLikePattern only for places that build raw
SQL LIKE clauses.

Comment on lines +669 to +693
filterOptions: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
const [names, tags, labels] = await Promise.all([
ctx.prisma.oxfordView.groupBy({
where: { projectId: input.projectId },
by: ["name"],
take: 1000,
orderBy: { name: "asc" },
}),
ctx.prisma.$queryRaw<{ value: string }[]>`
SELECT tags.tag as value
FROM oxford_views, UNNEST(oxford_views.tags) AS tags(tag)
WHERE oxford_views.project_id = ${input.projectId}
GROUP BY tags.tag
ORDER BY tags.tag ASC;
`,
ctx.prisma.$queryRaw<{ value: string }[]>`
SELECT labels.label as value
FROM oxford_views, UNNEST(oxford_views.labels) AS labels(label)
WHERE oxford_views.project_id = ${input.projectId}
GROUP BY labels.label
ORDER BY labels.label ASC;
`,
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Enforce project RBAC in filterOptions.

This query skips throwIfNoProjectAccess, so any authenticated caller can enumerate names, tags, and labels for an arbitrary projectId.

Suggested fix
   filterOptions: protectedProjectProcedure
     .input(z.object({ projectId: z.string() }))
     .query(async ({ input, ctx }) => {
+      throwIfNoProjectAccess({
+        session: ctx.session,
+        projectId: input.projectId,
+        scope: "prompts:read",
+      });
+
       const [names, tags, labels] = await Promise.all([
         ctx.prisma.oxfordView.groupBy({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
filterOptions: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
const [names, tags, labels] = await Promise.all([
ctx.prisma.oxfordView.groupBy({
where: { projectId: input.projectId },
by: ["name"],
take: 1000,
orderBy: { name: "asc" },
}),
ctx.prisma.$queryRaw<{ value: string }[]>`
SELECT tags.tag as value
FROM oxford_views, UNNEST(oxford_views.tags) AS tags(tag)
WHERE oxford_views.project_id = ${input.projectId}
GROUP BY tags.tag
ORDER BY tags.tag ASC;
`,
ctx.prisma.$queryRaw<{ value: string }[]>`
SELECT labels.label as value
FROM oxford_views, UNNEST(oxford_views.labels) AS labels(label)
WHERE oxford_views.project_id = ${input.projectId}
GROUP BY labels.label
ORDER BY labels.label ASC;
`,
]);
filterOptions: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
scope: "prompts:read",
});
const [names, tags, labels] = await Promise.all([
ctx.prisma.oxfordView.groupBy({
where: { projectId: input.projectId },
by: ["name"],
take: 1000,
orderBy: { name: "asc" },
}),
ctx.prisma.$queryRaw<{ value: string }[]>`
SELECT tags.tag as value
FROM oxford_views, UNNEST(oxford_views.tags) AS tags(tag)
WHERE oxford_views.project_id = ${input.projectId}
GROUP BY tags.tag
ORDER BY tags.tag ASC;
`,
ctx.prisma.$queryRaw<{ value: string }[]>`
SELECT labels.label as value
FROM oxford_views, UNNEST(oxford_views.labels) AS labels(label)
WHERE oxford_views.project_id = ${input.projectId}
GROUP BY labels.label
ORDER BY labels.label ASC;
`,
]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` around
lines 669 - 693, The filterOptions resolver currently queries project data
without enforcing project RBAC; before running the Promise.all, call the
existing project access guard (throwIfNoProjectAccess) using the current context
and input.projectId to validate the caller has access to that project (e.g.,
await ctx.project.throwIfNoProjectAccess(input.projectId)); then proceed with
the three ctx.prisma queries (oxfordView.groupBy and the two
ctx.prisma.$queryRaw calls) so only authorized callers can enumerate names,
tags, and labels.

Comment on lines +727 to +737
const escapedPathPrefix = pathPrefix
? escapeSqlLikePattern(pathPrefix)
: undefined;

const views = await ctx.prisma.oxfordView.findMany({
where: {
projectId,
name: promptName
? promptName
: { startsWith: `${escapedPathPrefix}/` },
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't escape pathPrefix before startsWith.

This has the same literal-mismatch bug as duplicateFolder: a folder name containing % or _ will no longer match the Prisma filter, so deletes can silently miss valid Oxford Views.

Suggested fix
-      const escapedPathPrefix = pathPrefix
-        ? escapeSqlLikePattern(pathPrefix)
-        : undefined;
-
       const views = await ctx.prisma.oxfordView.findMany({
         where: {
           projectId,
           name: promptName
             ? promptName
-            : { startsWith: `${escapedPathPrefix}/` },
+            : { startsWith: `${pathPrefix}/` },
         },
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const escapedPathPrefix = pathPrefix
? escapeSqlLikePattern(pathPrefix)
: undefined;
const views = await ctx.prisma.oxfordView.findMany({
where: {
projectId,
name: promptName
? promptName
: { startsWith: `${escapedPathPrefix}/` },
},
const views = await ctx.prisma.oxfordView.findMany({
where: {
projectId,
name: promptName
? promptName
: { startsWith: `${pathPrefix}/` },
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` around
lines 727 - 737, The code escapes pathPrefix into escapedPathPrefix and then
uses that in the Prisma startsWith filter, which causes literal-mismatch for
folder names containing SQL wildcard characters; change the logic in the
oxfordView.findMany where clause to use the original pathPrefix (not
escapeSqlLikePattern) for the startsWith comparison (i.e. use pathPrefix ?
`${pathPrefix}/` : undefined or similar) and only apply escapeSqlLikePattern
where a SQL LIKE pattern is actually required elsewhere; update references to
escapedPathPrefix accordingly and keep promptName handling the same.

persist-credentials: false

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

- name: Build and push ${{ matrix.component }}
uses: docker/build-push-action@v6

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v3

- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v5
@napan24 napan24 changed the title Oxford views integration feat(web): add oxford views prompt management May 26, 2026
OxfordView Prisma model lacks isActive, createdAt, updatedAt, projectId,
tags, and other fields that the Prompt domain type requires. Use `as any`
casts at call sites (PromptVersionDiffDialog, ReviewPromptDialog) to
unblock the Docker build without widening the shared Prompt type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
web/src/features/oxford-views/components/oxford-view-history.tsx (1)

126-127: ⚡ Quick win

Unsafe as any casts may cause runtime errors if the dialog accesses missing Prompt fields.

PromptVersionDiffDialog expects Prompt objects with fields like isActive, createdAt, updatedAt, projectId, and tags that OxfordViewVersion lacks. If the dialog accesses these properties, it will receive undefined values, potentially causing crashes or incorrect rendering.

Consider creating a shared interface or an adapter function that maps OxfordViewVersion to the expected shape with sensible defaults for missing fields:

Example adapter approach
// In a shared location
function toPromptLike(view: OxfordViewVersion): PromptLike {
  return {
    ...view,
    isActive: false,
    projectId: "", // or pass from props
    tags: [],
    // other required fields with defaults
  };
}
 <PromptVersionDiffDialog
   isOpen={isDiffOpen}
   setIsOpen={(open) => setIsDiffOpen(open)}
-  leftPrompt={view as any}
-  rightPrompt={props.currentView as any}
+  leftPrompt={toPromptLike(view)}
+  rightPrompt={toPromptLike(props.currentView)}
 />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/oxford-view-history.tsx` around
lines 126 - 127, The leftPrompt/rightPrompt props currently cast
OxfordViewVersion to any when rendering PromptVersionDiffDialog, which can
supply missing fields and cause runtime errors; create an adapter function
(e.g., toPromptLike) that maps OxfordViewVersion to the dialog's expected Prompt
shape (or a PromptLike interface) and supply sensible defaults for fields like
isActive, createdAt/updatedAt, projectId, and tags, then pass the adapted
objects to PromptVersionDiffDialog instead of using as any.
web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx (1)

420-435: ⚡ Quick win

Same as any cast issue—ReviewPromptDialog may access fields missing from OxfordViewLike.

The OxfordViewLike type (lines 34-42) explicitly omits fields that ReviewPromptDialog likely expects (e.g., isActive, createdAt, projectId). Casting to any hides these mismatches at compile time but may cause runtime issues.

Use the same adapter approach recommended for PromptVersionDiffDialog to provide defaults for missing fields rather than bypassing type safety.

Suggested fix
 <ReviewPromptDialog
-  initialPrompt={initialPrompt as any}
+  initialPrompt={toPromptLike(initialPrompt)}
   getNewPromptValues={form.getValues}
   isLoading={createMutation.isPending}
   onConfirm={form.handleSubmit(onSubmit)}
 >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx` around
lines 420 - 435, initialPrompt is being cast to any when passed to
ReviewPromptDialog which hides missing fields from OxfordViewLike; create an
adapter function (similar to the one used for PromptVersionDiffDialog) that maps
an OxfordViewLike into the full shape ReviewPromptDialog expects by supplying
default values for omitted fields (e.g., isActive, createdAt, projectId, etc.),
then pass the adapted value (not as any) into ReviewPromptDialog and keep
getNewPromptValues={form.getValues} and onConfirm={form.handleSubmit(onSubmit)}
unchanged so type-safety is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx`:
- Around line 420-435: initialPrompt is being cast to any when passed to
ReviewPromptDialog which hides missing fields from OxfordViewLike; create an
adapter function (similar to the one used for PromptVersionDiffDialog) that maps
an OxfordViewLike into the full shape ReviewPromptDialog expects by supplying
default values for omitted fields (e.g., isActive, createdAt, projectId, etc.),
then pass the adapted value (not as any) into ReviewPromptDialog and keep
getNewPromptValues={form.getValues} and onConfirm={form.handleSubmit(onSubmit)}
unchanged so type-safety is preserved.

In `@web/src/features/oxford-views/components/oxford-view-history.tsx`:
- Around line 126-127: The leftPrompt/rightPrompt props currently cast
OxfordViewVersion to any when rendering PromptVersionDiffDialog, which can
supply missing fields and cause runtime errors; create an adapter function
(e.g., toPromptLike) that maps OxfordViewVersion to the dialog's expected Prompt
shape (or a PromptLike interface) and supply sensible defaults for fields like
isActive, createdAt/updatedAt, projectId, and tags, then pass the adapted
objects to PromptVersionDiffDialog instead of using as any.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e923690-88e6-416c-9d85-8d806d3f81b7

📥 Commits

Reviewing files that changed from the base of the PR and between 4470117 and 8bc580e.

📒 Files selected for processing (2)
  • web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx
  • web/src/features/oxford-views/components/oxford-view-history.tsx

napan.vijayvargiya and others added 3 commits May 27, 2026 01:08
jsonSchema is exported from @langfuse/shared, not @langfuse/shared/src/server.
Fixes Docker build Type error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
oxford_views is not in the allowed table name union type; the oxford views
router reuses the same prompts table column definitions so 'prompts' is
the correct alias.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d type tabs

- Remove Text/Chat type selector — Oxford Views only accept string arrays
- Remove the Config JSON field entirely
- Replace content editor with a dynamic add/remove string entry list
- Update validation schema to z.array(z.string())
- Update tRPC create procedure to use dedicated CreateOxfordViewSchema
- Hardcode type='text' and config={} on write; remove PromptContentSchema
  and jsonSchema parse calls from read/copy paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
web/src/features/oxford-views/server/routers/oxfordViewRouter.ts (3)

729-739: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t escape pathPrefix for Prisma startsWith in delete flow.

Using escaped values in Prisma prefix matching can miss valid folder names containing % or _.

Suggested fix
-      const escapedPathPrefix = pathPrefix
-        ? escapeSqlLikePattern(pathPrefix)
-        : undefined;
-
       const views = await ctx.prisma.oxfordView.findMany({
         where: {
           projectId,
           name: promptName
             ? promptName
-            : { startsWith: `${escapedPathPrefix}/` },
+            : { startsWith: `${pathPrefix}/` },
         },
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` around
lines 729 - 739, The delete flow currently uses escapedPathPrefix (produced by
escapeSqlLikePattern) for Prisma startsWith, which causes matches to miss folder
names containing '%' or '_'; change the logic in the
ctx.prisma.oxfordView.findMany call so that when promptName is falsy you pass
the raw pathPrefix (not escapedPathPrefix) into the startsWith clause (or
undefined if pathPrefix is falsy), remove reliance on escapeSqlLikePattern for
this Prisma query, and keep promptName handling unchanged; locate the code
around escapedPathPrefix, escapeSqlLikePattern, pathPrefix, promptName and
ctx.prisma.oxfordView.findMany to make this change.

671-675: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Enforce RBAC before returning filter options.

filterOptions reads project-scoped names/tags/labels without throwIfNoProjectAccess, allowing metadata enumeration by unauthorized users.

Suggested fix
  filterOptions: protectedProjectProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ input, ctx }) => {
+      throwIfNoProjectAccess({
+        session: ctx.session,
+        projectId: input.projectId,
+        scope: "prompts:read",
+      });
+
       const [names, tags, labels] = await Promise.all([
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` around
lines 671 - 675, The filterOptions query (defined on protectedProjectProcedure
as filterOptions) fetches project-scoped names/tags/labels before enforcing
RBAC; call the existing RBAC guard throwIfNoProjectAccess for the incoming
input.projectId at the start of the .query handler (before any ctx.prisma calls)
so unauthorized users cannot enumerate metadata — i.e., invoke
throwIfNoProjectAccess(input.projectId, ctx) or the project access helper
available on ctx, await it, then proceed with the ctx.prisma.groupBy calls to
return names/tags/labels.

573-588: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use raw path values for Prisma startsWith in folder duplication.

Escaping sourcePath/targetPath before Prisma startsWith causes false negatives for names containing % or _.

Suggested fix
-      const escapedTarget = escapeSqlLikePattern(targetPath);
-      const escapedSource = escapeSqlLikePattern(sourcePath);
-
       const existingTarget = await ctx.prisma.oxfordView.findFirst({
-        where: { projectId, name: { startsWith: `${escapedTarget}/` } },
+        where: { projectId, name: { startsWith: `${targetPath}/` } },
       });
@@
       const sourceViews = await ctx.prisma.oxfordView.findMany({
-        where: { projectId, name: { startsWith: `${escapedSource}/` } },
+        where: { projectId, name: { startsWith: `${sourcePath}/` } },
         orderBy: [{ name: "asc" }, { version: "asc" }],
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` around
lines 573 - 588, The code is escaping sourcePath/targetPath via
escapeSqlLikePattern before passing to Prisma's startsWith, which breaks matches
containing '%' or '_'; update the queries that use
ctx.prisma.oxfordView.findFirst (existingTarget) and findMany (sourceViews) to
use the raw targetPath and sourcePath (e.g., `${targetPath}/` and
`${sourcePath}/`) for the startsWith conditions instead of
escapedTarget/escapedSource, and keep escapeSqlLikePattern only for raw SQL LIKE
usages if any.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts`:
- Line 34: CreateOxfordViewSchema currently allows an empty prompt array; update
the prompt schema (where prompt: z.array(z.string().min(1))) to require at least
one entry by adding an array length check (e.g., prompt:
z.array(z.string().min(1)).nonempty() or prompt:
z.array(z.string().min(1)).min(1)) so API callers cannot create views with an
empty prompt.

---

Duplicate comments:
In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts`:
- Around line 729-739: The delete flow currently uses escapedPathPrefix
(produced by escapeSqlLikePattern) for Prisma startsWith, which causes matches
to miss folder names containing '%' or '_'; change the logic in the
ctx.prisma.oxfordView.findMany call so that when promptName is falsy you pass
the raw pathPrefix (not escapedPathPrefix) into the startsWith clause (or
undefined if pathPrefix is falsy), remove reliance on escapeSqlLikePattern for
this Prisma query, and keep promptName handling unchanged; locate the code
around escapedPathPrefix, escapeSqlLikePattern, pathPrefix, promptName and
ctx.prisma.oxfordView.findMany to make this change.
- Around line 671-675: The filterOptions query (defined on
protectedProjectProcedure as filterOptions) fetches project-scoped
names/tags/labels before enforcing RBAC; call the existing RBAC guard
throwIfNoProjectAccess for the incoming input.projectId at the start of the
.query handler (before any ctx.prisma calls) so unauthorized users cannot
enumerate metadata — i.e., invoke throwIfNoProjectAccess(input.projectId, ctx)
or the project access helper available on ctx, await it, then proceed with the
ctx.prisma.groupBy calls to return names/tags/labels.
- Around line 573-588: The code is escaping sourcePath/targetPath via
escapeSqlLikePattern before passing to Prisma's startsWith, which breaks matches
containing '%' or '_'; update the queries that use
ctx.prisma.oxfordView.findFirst (existingTarget) and findMany (sourceViews) to
use the raw targetPath and sourcePath (e.g., `${targetPath}/` and
`${sourcePath}/`) for the startsWith conditions instead of
escapedTarget/escapedSource, and keep escapeSqlLikePattern only for raw SQL LIKE
usages if any.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5245d9c3-f3c4-4378-ac9c-fefb3483d9b2

📥 Commits

Reviewing files that changed from the base of the PR and between cb6c26f and 590869f.

📒 Files selected for processing (3)
  • web/src/features/oxford-views/components/NewOxfordViewForm/index.tsx
  • web/src/features/oxford-views/components/NewOxfordViewForm/validation.ts
  • web/src/features/oxford-views/server/routers/oxfordViewRouter.ts

const CreateOxfordViewSchema = z.object({
projectId: z.string(),
name: PromptNameSchema,
prompt: z.array(z.string().min(1)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require at least one prompt entry in the server schema.

CreateOxfordViewSchema currently accepts an empty prompt array, so direct API callers can create empty views even though the UI blocks it.

Suggested fix
-  prompt: z.array(z.string().min(1)),
+  prompt: z.array(z.string().min(1)).min(1, "Add at least one entry"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
prompt: z.array(z.string().min(1)),
prompt: z.array(z.string().min(1)).min(1, "Add at least one entry"),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/oxford-views/server/routers/oxfordViewRouter.ts` at line 34,
CreateOxfordViewSchema currently allows an empty prompt array; update the prompt
schema (where prompt: z.array(z.string().min(1))) to require at least one entry
by adding an array length check (e.g., prompt:
z.array(z.string().min(1)).nonempty() or prompt:
z.array(z.string().min(1)).min(1)) so API callers cannot create views with an
empty prompt.

napan.vijayvargiya and others added 3 commits May 27, 2026 10:25
OxfordView.config is JsonValue (includes null) but createMany input
requires InputJsonValue. Fallback to empty object fixes the type error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JsonValue includes null which is not assignable to InputJsonValue.
Cast (v.config ?? {}) as Prisma.InputJsonValue in both copy/duplicate
procedures and fix the toCreate type annotation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/public/oxford-views and GET /api/public/oxford-views/:viewName
fetch a view by name/label/version; POST creates a new version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
web/src/pages/api/public/oxford-views/[viewName]/index.ts (1)

32-125: 🏗️ Heavy lift

Consider using withMiddlewares for public API routes.

Per coding guidelines, public API routes in src/pages/api/public/** should use src/features/public-api/server/withMiddlewares.ts rather than implementing the middleware chain manually. This ensures consistent auth, CORS, rate limiting, and error handling patterns across all public endpoints.

Additionally, strict request and response types should be defined in src/features/public-api/types/* for API contract consistency.

As per coding guidelines: "Public API routes should use src/features/public-api/server/withMiddlewares.ts, define strict request and response types in src/features/public-api/types/*, add server tests, and update Fern sources when the contract changes"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/api/public/oxford-views/`[viewName]/index.ts around lines 32 -
125, The handler in this public API route manually runs cors, auth, rate
limiting, telemetry and error handling; replace this pattern by wrapping the
exported handler with the centralized withMiddlewares so the route uses
src/features/public-api/server/withMiddlewares.ts for CORS/auth/rate limits and
unified error handling. Refactor the current exported async function handler to
a lean business-logic function (preserving logic that looks up the oxfordView,
e.g., QuerySchema.parse, prisma.oxfordView queries, PRODUCTION_LABEL usage and
the JSON response shape) and export it via withMiddlewares(...) so
ApiAuthService/RateLimitService specifics are handled by the middleware; also
add/consume the strict request/response types from src/features/public-api/types
to define the route contract and update tests/Fern sources as needed.
web/src/pages/api/public/oxford-views/index.ts (1)

46-224: 🏗️ Heavy lift

Consider using withMiddlewares for public API routes.

Same as the [viewName] endpoint—this route should use src/features/public-api/server/withMiddlewares.ts and define strict request/response types in src/features/public-api/types/* per the coding guidelines for public API routes.

As per coding guidelines: "Public API routes should use src/features/public-api/server/withMiddlewares.ts, define strict request and response types in src/features/public-api/types/*, add server tests, and update Fern sources when the contract changes"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/api/public/oxford-views/index.ts` around lines 46 - 224, The
handler function currently implements middleware, auth, validation and response
logic inline; refactor it to use the public API helper by wrapping the exported
handler with withMiddlewares (from
src/features/public-api/server/withMiddlewares.ts) and move request/response
shapes into strict types/schemas under src/features/public-api/types (create
GetOxfordViewRequest/Response and CreateOxfordViewRequest/Response types or Zod
schemas), update handler signature to accept typed req/res or a typed context as
required by withMiddlewares, replace inline runMiddleware/cors usage with the
withMiddlewares pipeline, and ensure the route still calls ApiAuthService,
RateLimitService, prisma lookups (oxfordView queries), and error handling
unchanged; also add/adjust server tests and Fern contract updates to reflect the
new typed request/response contracts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/pages/api/public/oxford-views/index.ts`:
- Around line 151-158: The current Promise.all over previousWithProduction
calling prisma.oxfordView.update can leave partial updates if one update fails;
replace this with a transactional single-operation approach—either run the
per-record updates inside a prisma.$transaction (use
prisma.$transaction(previousWithProduction.map(v =>
prisma.oxfordView.update(...))) so all updates commit or roll back together) or
perform a single update via a raw query within a transaction (e.g. use
tx.$executeRaw to array_remove PRODUCTION_LABEL from labels for matching
projectId/name/ANY(labels)). Ensure you reference previousWithProduction,
PRODUCTION_LABEL, and prisma.oxfordView.update (or tx.$executeRaw) when making
the change.
- Around line 134-185: The current flow that computes newVersion by calling
prisma.oxfordView.findFirst and then updates/removes labels and creates a new
row (prisma.oxfordView.findMany, prisma.oxfordView.update,
prisma.oxfordView.create) is racy and not transactional; wrap the multi-step
logic in a single Prisma transaction to obtain atomicity and use row-level
locking or a DB-unique constraint to prevent duplicate versions: start a
transaction, re-read the latest row for (projectId, name) inside the transaction
(or rely on a unique constraint on (projectId, name, version) so conflicting
creates fail), perform the label updates and create the new row within that
transaction, and handle transaction errors by retrying/create-failure detection
so concurrent requests either serialize or one fails cleanly. Ensure the
functions referenced (the blocks using prisma.oxfordView.findFirst, findMany,
update, create) are moved inside the transaction scope and add/ensure a database
unique constraint on (projectId, name, version) to provide a final safeguard.

---

Nitpick comments:
In `@web/src/pages/api/public/oxford-views/`[viewName]/index.ts:
- Around line 32-125: The handler in this public API route manually runs cors,
auth, rate limiting, telemetry and error handling; replace this pattern by
wrapping the exported handler with the centralized withMiddlewares so the route
uses src/features/public-api/server/withMiddlewares.ts for CORS/auth/rate limits
and unified error handling. Refactor the current exported async function handler
to a lean business-logic function (preserving logic that looks up the
oxfordView, e.g., QuerySchema.parse, prisma.oxfordView queries, PRODUCTION_LABEL
usage and the JSON response shape) and export it via withMiddlewares(...) so
ApiAuthService/RateLimitService specifics are handled by the middleware; also
add/consume the strict request/response types from src/features/public-api/types
to define the route contract and update tests/Fern sources as needed.

In `@web/src/pages/api/public/oxford-views/index.ts`:
- Around line 46-224: The handler function currently implements middleware,
auth, validation and response logic inline; refactor it to use the public API
helper by wrapping the exported handler with withMiddlewares (from
src/features/public-api/server/withMiddlewares.ts) and move request/response
shapes into strict types/schemas under src/features/public-api/types (create
GetOxfordViewRequest/Response and CreateOxfordViewRequest/Response types or Zod
schemas), update handler signature to accept typed req/res or a typed context as
required by withMiddlewares, replace inline runMiddleware/cors usage with the
withMiddlewares pipeline, and ensure the route still calls ApiAuthService,
RateLimitService, prisma lookups (oxfordView queries), and error handling
unchanged; also add/adjust server tests and Fern contract updates to reflect the
new typed request/response contracts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 27e6ec43-f589-4d3b-a4d0-d0c80bee4f62

📥 Commits

Reviewing files that changed from the base of the PR and between 590869f and 7b4ade5.

📒 Files selected for processing (3)
  • web/src/features/oxford-views/server/routers/oxfordViewRouter.ts
  • web/src/pages/api/public/oxford-views/[viewName]/index.ts
  • web/src/pages/api/public/oxford-views/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/features/oxford-views/server/routers/oxfordViewRouter.ts

Comment on lines +134 to +185
const latest = await prisma.oxfordView.findFirst({
where: { projectId, name: input.name },
orderBy: { version: "desc" },
});

const newVersion = (latest?.version ?? 0) + 1;
const finalLabels = [...new Set([...input.labels, LATEST_PROMPT_LABEL])];

// Promote production label: strip it from previous versions if present
if (finalLabels.includes(PRODUCTION_LABEL)) {
const previousWithProduction = await prisma.oxfordView.findMany({
where: {
projectId,
name: input.name,
labels: { has: PRODUCTION_LABEL },
},
});
await Promise.all(
previousWithProduction.map((v) =>
prisma.oxfordView.update({
where: { id: v.id },
data: { labels: v.labels.filter((l) => l !== PRODUCTION_LABEL) },
}),
),
);
}

// Strip LATEST_PROMPT_LABEL from the previous latest version
if (latest) {
await prisma.oxfordView.update({
where: { id: latest.id },
data: {
labels: latest.labels.filter((l) => l !== LATEST_PROMPT_LABEL),
},
});
}

const created = await prisma.oxfordView.create({
data: {
id: uuidv4(),
name: input.name,
prompt: input.prompt,
version: newVersion,
labels: finalLabels,
type: "text",
config: {},
tags: [],
commitMessage: input.commitMessage ?? null,
createdBy: "API",
project: { connect: { id: projectId } },
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Race condition: concurrent POST requests can create duplicate version numbers.

The version calculation reads the latest version and increments it without any locking or atomic operation. Two concurrent requests for the same view name will both read the same latest.version, compute the same newVersion, and create records with duplicate version numbers.

Additionally, the multi-step operation (find latest → update previous labels → create new) is not wrapped in a transaction. If any intermediate step fails, the database state can become inconsistent (e.g., LATEST_PROMPT_LABEL stripped from old version but new version not created).

🔒 Recommended fix: wrap in a transaction with row-level locking
-      const latest = await prisma.oxfordView.findFirst({
-        where: { projectId, name: input.name },
-        orderBy: { version: "desc" },
-      });
-
-      const newVersion = (latest?.version ?? 0) + 1;
-      const finalLabels = [...new Set([...input.labels, LATEST_PROMPT_LABEL])];
-
-      // Promote production label: strip it from previous versions if present
-      if (finalLabels.includes(PRODUCTION_LABEL)) {
-        const previousWithProduction = await prisma.oxfordView.findMany({
+      const created = await prisma.$transaction(async (tx) => {
+        // Lock rows for this project+name to prevent concurrent version conflicts
+        const latest = await tx.oxfordView.findFirst({
+          where: { projectId, name: input.name },
+          orderBy: { version: "desc" },
+        });
+
+        const newVersion = (latest?.version ?? 0) + 1;
+        const finalLabels = [...new Set([...input.labels, LATEST_PROMPT_LABEL])];
+
+        // Promote production label: strip it from previous versions if present
+        if (finalLabels.includes(PRODUCTION_LABEL)) {
+          await tx.oxfordView.updateMany({
+            where: {
+              projectId,
+              name: input.name,
+              labels: { has: PRODUCTION_LABEL },
+            },
+            data: {
+              // Note: updateMany doesn't support array filtering directly;
+              // consider using raw SQL or iterating within the transaction
+            },
+          });
+        }
+
+        // Strip LATEST_PROMPT_LABEL from the previous latest version
+        if (latest) {
+          await tx.oxfordView.update({
+            where: { id: latest.id },
+            data: {
+              labels: latest.labels.filter((l) => l !== LATEST_PROMPT_LABEL),
+            },
+          });
+        }
+
+        return tx.oxfordView.create({
+          data: {
+            id: uuidv4(),
+            name: input.name,
+            prompt: input.prompt,
+            version: newVersion,
+            labels: finalLabels,
+            type: "text",
+            config: {},
+            tags: [],
+            commitMessage: input.commitMessage ?? null,
+            createdBy: "API",
+            project: { connect: { id: projectId } },
+          },
+        });
+      });

For stronger guarantees against version conflicts, consider adding a unique constraint on (projectId, name, version) in the database schema, which would cause concurrent duplicates to fail at the DB level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/api/public/oxford-views/index.ts` around lines 134 - 185, The
current flow that computes newVersion by calling prisma.oxfordView.findFirst and
then updates/removes labels and creates a new row (prisma.oxfordView.findMany,
prisma.oxfordView.update, prisma.oxfordView.create) is racy and not
transactional; wrap the multi-step logic in a single Prisma transaction to
obtain atomicity and use row-level locking or a DB-unique constraint to prevent
duplicate versions: start a transaction, re-read the latest row for (projectId,
name) inside the transaction (or rely on a unique constraint on (projectId,
name, version) so conflicting creates fail), perform the label updates and
create the new row within that transaction, and handle transaction errors by
retrying/create-failure detection so concurrent requests either serialize or one
fails cleanly. Ensure the functions referenced (the blocks using
prisma.oxfordView.findFirst, findMany, update, create) are moved inside the
transaction scope and add/ensure a database unique constraint on (projectId,
name, version) to provide a final safeguard.

Comment on lines +151 to +158
await Promise.all(
previousWithProduction.map((v) =>
prisma.oxfordView.update({
where: { id: v.id },
data: { labels: v.labels.filter((l) => l !== PRODUCTION_LABEL) },
}),
),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Promise.all can leave partial updates on failure.

If one of the update calls in Promise.all fails while others succeed, some records will have PRODUCTION_LABEL stripped while others retain it. Consider using updateMany with a single query or ensuring all updates are within the same transaction.

♻️ Suggested improvement using updateMany pattern

Note: Prisma's updateMany doesn't directly support array element removal. Within a transaction, you could use raw SQL or keep the iteration but ensure it's all within a single transaction scope (as suggested above).

Alternatively, if the label array manipulation is complex, consider a raw query:

await tx.$executeRaw`
  UPDATE "OxfordView"
  SET labels = array_remove(labels, ${PRODUCTION_LABEL})
  WHERE "projectId" = ${projectId}
    AND name = ${input.name}
    AND ${PRODUCTION_LABEL} = ANY(labels)
`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/pages/api/public/oxford-views/index.ts` around lines 151 - 158, The
current Promise.all over previousWithProduction calling prisma.oxfordView.update
can leave partial updates if one update fails; replace this with a transactional
single-operation approach—either run the per-record updates inside a
prisma.$transaction (use prisma.$transaction(previousWithProduction.map(v =>
prisma.oxfordView.update(...))) so all updates commit or roll back together) or
perform a single update via a raw query within a transaction (e.g. use
tx.$executeRaw to array_remove PRODUCTION_LABEL from labels for matching
projectId/name/ANY(labels)). Ensure you reference previousWithProduction,
PRODUCTION_LABEL, and prisma.oxfordView.update (or tx.$executeRaw) when making
the change.

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