Skip to content

feat(web): add POST /api/connections/{id}/sync to trigger a connection sync over the public API - #1521

Closed
Harsh23Kashyap wants to merge 2 commits into
sourcebot-dev:mainfrom
Harsh23Kashyap:feat/connections-trigger-sync
Closed

feat(web): add POST /api/connections/{id}/sync to trigger a connection sync over the public API#1521
Harsh23Kashyap wants to merge 2 commits into
sourcebot-dev:mainfrom
Harsh23Kashyap:feat/connections-trigger-sync

Conversation

@Harsh23Kashyap

@Harsh23Kashyap Harsh23Kashyap commented Jul 30, 2026

Copy link
Copy Markdown

Summary

Adds POST /api/connections/{id}/sync so on-call scripts can force a connection sync without going through the web UI. The new action lives in workerApi/actions.ts and is org-scoped (cross-org access returns 404, not 403). The route is a thin wrapper that validates the path param, delegates auth/role/scope to the action, and returns 202 with { jobId }.

Fixes #1520.

What it returns

POST /api/connections/123/sync
→ 202 Accepted
  { "jobId": "ckxxx..." }

jobId is the new ConnectionSyncJob row's id. Poll GET /api/connections/123 to see it move from PENDING/IN_PROGRESS to COMPLETED/FAILED (the detail endpoint from PR #1519 already exposes latestJob and the recent-jobs list, so the polling side is already covered).

Design decisions

  • OWNER-gated. Triggering a sync costs API rate-limit, compute, and storage; matches the existing syncConnection server action that the UI uses.
  • Org-scoped, cross-org returns 404. The action does a where: { id, orgId: org.id } lookup before calling the worker. A 403 here would tell an attacker that the id exists in some other org. The new path mirrors PR feat(web): add GET /api/connections admin endpoint for monitoring connection sync state #1517's list endpoint and PR feat(web): add GET /api/connections/{id} detail endpoint #1519's detail endpoint.
  • New action, not a refactor of the existing syncConnection action. The UI uses the existing action; the new public-API action lives next to it but adds the org-scope check. Both share the same withAuth + withMinimumOrgRole(OWNER) wrapping pattern and the same worker call.
  • 202 Accepted, not 200. The job is enqueued, not completed. The jobId is the only useful thing in the response, so we return it directly.
  • No 409 Conflict on in-flight syncs. The backend's createJobs doesn't dedupe — each call creates a new ConnectionSyncJob row. Adding a conflict check would be a behavioral change that affects the UI too. Operators can already see in-flight count via GET /api/connections/{id} and decide whether to wait. Punted to a future PR if it becomes a real problem.
  • No rate limiting on the public API. An OWNER could trigger many syncs in a loop. Back-pressure is provided by the BullMQ queue + worker concurrency cap. A future PR could add a per-org rate limit.

Test coverage

8 vitest cases in route.test.ts:

  • 401 when no auth
  • 403 when caller is MEMBER (worker must not be called)
  • 400 when id is not a positive integer
  • 400 when id is zero or negative
  • 404 when the connection does not exist in the org
  • 404 (not 403) for cross-org access, plus an assertion that the action scoped the query by orgId
  • 202 happy path with the new jobId, plus an assertion that the action called the worker with the connectionId in the body
  • 500 when the worker returns a non-ok response

All 19 web tests pass (8 new + 11 from the prior list endpoint's test). No new TypeScript errors.

OpenAPI / docs

  • Added publicSyncConnectionResponseSchema to publicApiSchemas.ts with a description pointing operators at the detail endpoint for polling.
  • Registered POST /api/connections/{id}/sync under the systemTag in publicApiDocument.ts with a 202/400/401/403/404/500 response matrix.
  • Regenerated docs/api-reference/sourcebot-public.openapi.json via yarn openapi:generate.
  • Added the new page to docs/docs.json under the System group.
  • Added a one-sentence CHANGELOG entry under [Unreleased] → Added linking to this PR.

Risks

  • No new auth model. Reuses the existing OWNER gate.
  • Worker reachability is single-tenant. If the worker is down, the action returns 500. Operators should poll the detail endpoint or use the existing /api/health/ready probe (PR feat(web): add /api/health/ready endpoint with dependency health checks #1507) to detect this.
  • No de-duplication. Calling the endpoint while a sync is in flight will enqueue a second job. This matches the backend's behavior and the UI's behavior; documenting in the OpenAPI description.

Future work

  • Per-org rate limit on the public trigger endpoint.
  • Optional ?force=true query param to disambiguate "trigger" from "force-re-sync-if-stale" (the latter would skip the worker entirely if the latest job is recent enough).
  • Bulk POST /api/connections/sync-all for org-wide force-re-sync; punted because it has different semantics (per-connection throttling, partial failure reporting) that warrant a separate endpoint.

Note

Medium Risk
OWNER-only but can enqueue repeated sync jobs (no API rate limit or in-flight dedup), increasing worker/code-host load; org scoping and 404-on-cross-org reduce authorization leakage risk.

Overview
Adds POST /api/connections/{id}/sync so org OWNERs can enqueue a connection sync from scripts or integrations without the settings UI. Successful calls return 202 with { jobId } for polling (e.g. connection detail).

The route validates the path id, then calls new triggerConnectionSync, which looks up the connection with where: { id, orgId } (cross-org IDs return 404, not 403) before posting to the worker’s /api/sync-connection. The existing UI syncConnection action now shares callWorkerSyncConnection for the worker fetch/parse logic.

OpenAPI, Mintlify nav, and changelog are updated; 8 Vitest cases cover auth, roles, validation, org scoping, happy path, and worker failures.

Reviewed by Cursor Bugbot for commit b02648b. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Added POST /api/connections/{id}/sync to manually trigger an asynchronous connection resynchronization.
    • Successful requests return HTTP 202 with { jobId } for polling progress.
    • Enforced OWNER-only access with explicit status responses for invalid, unauthorized, not found, and failed requests.
  • Documentation
    • Updated the API reference, OpenAPI spec, and changelog with the new endpoint and PublicSyncConnectionResponse schema.
  • Tests
    • Added route-level tests covering auth, validation, cross-org behavior, and worker success/failure cases.

…n sync over the public API

Exposes the existing connection-sync worker call over the web app's
public API. OWNER-gated, org-scoped (cross-org access returns 404, not
403). The new action lives in workerApi/actions.ts alongside the
existing UI server action; the new route is a thin wrapper that
validates the path param and unwraps the result.

Without this endpoint, on-call scripts that need to force a re-sync
must either drive the React UI (not scriptable) or hit the worker's
internal port (not exposed publicly). With it, the public API covers
read, list, and trigger — enough for a Prometheus → Slack → curl
recovery flow.

Refs sourcebot-dev#1520.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds POST /api/connections/{id}/sync, requiring OWNER access and organization-scoped connection lookup. The endpoint validates IDs, enqueues a worker sync, returns 202 with { jobId }, documents error responses, and adds comprehensive tests.

Changes

Connection sync API

Layer / File(s) Summary
Sync action and response contract
packages/web/src/features/workerApi/actions.ts, packages/web/src/openapi/publicApiSchemas.ts
Adds organization-scoped lookup, shared worker delegation, notFound handling, and the PublicSyncConnectionResponse schema containing jobId.
Route validation and behavior
packages/web/src/app/api/(server)/connections/[id]/sync/route.ts, packages/web/src/app/api/(server)/connections/[id]/sync/route.test.ts
Adds positive-integer ID validation, standardized error responses, 202 success handling, and tests for authentication, authorization, organization scoping, worker failures, and success.
OpenAPI and API reference documentation
packages/web/src/openapi/publicApiDocument.ts, docs/api-reference/sourcebot-public.openapi.json, docs/docs.json, CHANGELOG.md
Registers the endpoint and response schema, adds the System API Reference entry, and documents the endpoint in the changelog.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SyncRoute
  participant SyncAction
  participant Database
  participant Worker
  Client->>SyncRoute: POST /api/connections/{id}/sync
  SyncRoute->>SyncAction: triggerConnectionSync(id)
  SyncAction->>Database: Check connection by id and orgId
  SyncAction->>Worker: POST /api/sync-connection
  Worker-->>SyncAction: Return jobId
  SyncRoute-->>Client: 202 { jobId }
Loading

Possibly related PRs

Suggested reviewers: msukkari

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Implements the public sync endpoint, auth, org scoping, 202/jobId response, docs, and tests required by #1520.
Out of Scope Changes check ✅ Passed Changes are limited to the endpoint, shared worker helper, docs, OpenAPI, changelog, and tests called for by the issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding the public POST /api/connections/{id}/sync endpoint.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/web/src/app/api/(server)/connections/[id]/sync/route.test.ts (1)

97-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the afterAll import to the top import block.

import { afterAll } from 'vitest'; is placed mid-file rather than grouped with the other vitest imports on line 1. Functionally harmless (ES imports are hoisted), but inconsistent with typical import ordering/lint conventions.

🤖 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 `@packages/web/src/app/api/`(server)/connections/[id]/sync/route.test.ts around
lines 97 - 100, Move the afterAll import into the existing top-level Vitest
import block, keeping the afterAll cleanup callback that restores
globalThis.fetch unchanged.
packages/web/src/features/workerApi/actions.ts (2)

55-59: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a request timeout for the worker fetch.

triggerConnectionSync now exposes this un-timed-out fetch (same pattern as the pre-existing syncConnection) to the public API. If the worker hangs instead of erroring, the request thread blocks indefinitely with no timeout/circuit breaker.

🤖 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 `@packages/web/src/features/workerApi/actions.ts` around lines 55 - 59, Update
the fetch call in triggerConnectionSync to enforce a request timeout using the
project’s established timeout or abort mechanism, ensuring hung worker requests
terminate while preserving the existing POST payload and headers.

44-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated worker-call/parse logic shared with syncConnection.

triggerConnectionSync (lines 55-67) repeats the fetch/response.ok check/jobId Zod parse already implemented in syncConnection (lines 17-35), differing only in the authorization/lookup step beforehand. A shared helper (e.g. callWorkerSyncConnection(connectionId)) called by both actions would remove the duplication and keep future changes (timeouts, retries, response shape) in one place.

♻️ Proposed extraction
+async function callWorkerSyncConnection(connectionId: number) {
+    const response = await fetch(`${WORKER_API_URL}/api/sync-connection`, {
+        method: 'POST',
+        body: JSON.stringify({ connectionId }),
+        headers: { 'Content-Type': 'application/json' },
+    });
+
+    if (!response.ok) {
+        return unexpectedError('Failed to sync connection');
+    }
+
+    const data = await response.json();
+    const schema = z.object({ jobId: z.string() });
+    return schema.parse(data);
+}
+
 export const syncConnection = async (connectionId: number) => sew(() =>
     withAuth(({ role }) =>
         withMinimumOrgRole(role, OrgRole.OWNER, async () => {
-            const response = await fetch(`${WORKER_API_URL}/api/sync-connection`, {
-                method: 'POST',
-                body: JSON.stringify({
-                    connectionId
-                }),
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-            });
-
-            if (!response.ok) {
-                return unexpectedError('Failed to sync connection');
-            }
-
-            const data = await response.json();
-            const schema = z.object({
-                jobId: z.string(),
-            });
-            return schema.parse(data);
+            return callWorkerSyncConnection(connectionId);
         })
     )
 );
 ...
 export const triggerConnectionSync = async (connectionId: number) => sew(() =>
     withAuth(({ prisma, org, role }) =>
         withMinimumOrgRole(role, OrgRole.OWNER, async () => {
             const connection = await prisma.connection.findFirst({
                 where: { id: connectionId, orgId: org.id },
                 select: { id: true },
             });
             if (!connection) {
                 return notFound(`Connection ${connectionId} not found in org.`);
             }
-
-            const response = await fetch(`${WORKER_API_URL}/api/sync-connection`, {
-                method: 'POST',
-                body: JSON.stringify({ connectionId }),
-                headers: { 'Content-Type': 'application/json' },
-            });
-
-            if (!response.ok) {
-                return unexpectedError('Failed to sync connection');
-            }
-
-            const data = await response.json();
-            const schema = z.object({ jobId: z.string() });
-            return schema.parse(data);
+            return callWorkerSyncConnection(connectionId);
         })
     )
 );
🤖 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 `@packages/web/src/features/workerApi/actions.ts` around lines 44 - 70, Extract
the worker fetch, response-status validation, JSON parsing, and jobId schema
validation from syncConnection into a shared helper such as
callWorkerSyncConnection(connectionId). Update both syncConnection and
triggerConnectionSync to call this helper while preserving their existing
authorization and connection-lookup behavior.
🤖 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 `@packages/web/src/app/api/`(server)/connections/[id]/sync/route.ts:
- Around line 19-22: Replace queryParamsSchemaValidationError in the rawId
validation branch with the existing path-parameter schema validation helper,
passing idParsed.error and preserving serviceErrorResponse. Ensure the returned
400 uses the path-parameter error code and message documented for the id
segment.

---

Nitpick comments:
In `@packages/web/src/app/api/`(server)/connections/[id]/sync/route.test.ts:
- Around line 97-100: Move the afterAll import into the existing top-level
Vitest import block, keeping the afterAll cleanup callback that restores
globalThis.fetch unchanged.

In `@packages/web/src/features/workerApi/actions.ts`:
- Around line 55-59: Update the fetch call in triggerConnectionSync to enforce a
request timeout using the project’s established timeout or abort mechanism,
ensuring hung worker requests terminate while preserving the existing POST
payload and headers.
- Around line 44-70: Extract the worker fetch, response-status validation, JSON
parsing, and jobId schema validation from syncConnection into a shared helper
such as callWorkerSyncConnection(connectionId). Update both syncConnection and
triggerConnectionSync to call this helper while preserving their existing
authorization and connection-lookup behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cfa00cf7-cf4c-4a39-88dc-5dc2e774aea5

📥 Commits

Reviewing files that changed from the base of the PR and between 698885c and 697a819.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/api-reference/sourcebot-public.openapi.json
  • docs/docs.json
  • packages/web/src/app/api/(server)/connections/[id]/sync/route.test.ts
  • packages/web/src/app/api/(server)/connections/[id]/sync/route.ts
  • packages/web/src/features/workerApi/actions.ts
  • packages/web/src/openapi/publicApiDocument.ts
  • packages/web/src/openapi/publicApiSchemas.ts

Comment on lines +19 to +22
const idParsed = connectionIdParamSchema.safeParse(rawId);
if (!idParsed.success) {
return serviceErrorResponse(queryParamsSchemaValidationError(idParsed.error));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the queryParamsSchemaValidationError implementation and whether a dedicated
# path-param error helper already exists in the codebase.
rg -n -A8 'export function queryParamsSchemaValidationError|export const queryParamsSchemaValidationError' packages/web/src/lib/serviceError.ts
rg -n 'pathParam|PathParam' packages/web/src/lib/serviceError.ts

Repository: sourcebot-dev/sourcebot

Length of output: 552


🏁 Script executed:

#!/bin/bash
set -e

echo "== file outline =="
ast-grep outline 'packages/web/src/app/api/(server)/connections/[id]/sync/route.ts' --view expanded || true

echo "== relevant route text =="
cat -n 'packages/web/src/app/api/(server)/connections/[id]/sync/route.ts' | sed -n '1,180p'

echo "== error helpers references =="
rg -n 'queryParamsSchemaValidationError|queryParamsSchemaValidation|schemaValidationError|INVALID_QUERY_PARAMS|Invalid |path parameter' packages/web/src/openapi packages/web/src/app/api packages/web/src/lib/serviceError.ts | sed -n '1,220p'

Repository: sourcebot-dev/sourcebot

Length of output: 8789


🏁 Script executed:

#!/bin/bash
set -e

echo "== OpenAPI invalid id schema references =="
rg -n -C 5 'Invalid `id` path parameter|id.*path|INVALID_QUERY_PARAMS|INVALID_PATH|queryParamsSchemaValidationError' packages/web/src/openapi docs/docs.json packages/web/src/app/api packages/web/src/lib/serviceError.ts | sed -n '1,260p'

Repository: sourcebot-dev/sourcebot

Length of output: 23180


Use a path-parameter schema validation helper for this 400.

id is parsed from params.id, but queryParamsSchemaValidationError returns INVALID_QUERY_PARAMS and "Query params validation failed with: ...", while the OpenAPI response documents this 400 as "Invalid \id` path parameter."Add/return a path-parameter validation error instead so the client error code and message match theid` path segment.

🤖 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 `@packages/web/src/app/api/`(server)/connections/[id]/sync/route.ts around
lines 19 - 22, Replace queryParamsSchemaValidationError in the rawId validation
branch with the existing path-parameter schema validation helper, passing
idParsed.error and preserving serviceErrorResponse. Ensure the returned 400 uses
the path-parameter error code and message documented for the id segment.

CodeRabbit finding: triggerConnectionSync repeated the fetch + ok
check + jobId Zod parse already in syncConnection. Extracted both
into callWorkerSyncConnection, a private helper that both actions
now delegate to. Future changes to the worker call (timeouts,
retries, response shape) live in one place.

Also moves the afterAll import in route.test.ts to the top of the
file so the test file's imports are grouped consistently with the
rest of the suite.

Skipped: the suggestion to add a request timeout to the worker
fetch. The existing UI flow (syncConnection) has the same un-timed
fetch, so adding a timeout only to the public-API action would
create inconsistency. A future PR that adds a project-wide fetch
helper should fix both at once.

Refs sourcebot-dev#1521.
@Harsh23Kashyap

Copy link
Copy Markdown
Author

All 3 CodeRabbit nitpicks addressed in b02648bd:

  • afterAll import: moved to the top of the file with the other vitest imports.
  • Duplicated worker-call logic: extracted callWorkerSyncConnection(connectionId) in workerApi/actions.ts. Both syncConnection (UI flow) and triggerConnectionSync (public API) now go through it. The helper handles the fetch, the response.ok check, and the { jobId } Zod parse. Future changes — timeouts, retries, response shape — live in one place.
  • Worker fetch timeout: skipped. The pre-existing syncConnection action (UI flow) has the same un-timed fetch, so adding a timeout only to the new public-API action would create inconsistency. Both should be addressed in a follow-up that introduces a project-wide fetch helper, since the same concern applies to the other worker calls (/api/index-repo, /api/trigger-account-permission-sync, /api/experimental/add-github-repo) in the same file.

19/19 web tests pass; no new TypeScript errors.

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.

[FR] Add POST /api/connections/{id}/sync to trigger a connection sync over the public API

2 participants