Skip to content

feat(api): harden developer gear API and add catalog snapshot#367

Merged
Flohhhhh merged 9 commits into
mainfrom
development
Jul 15, 2026
Merged

feat(api): harden developer gear API and add catalog snapshot#367
Flohhhhh merged 9 commits into
mainfrom
development

Conversation

@Flohhhhh

Copy link
Copy Markdown
Owner

Summary

  • harden public gear serializers with explicit allowlists and taxonomy labels instead of internal UUIDs
  • add cached GET /api/v1/catalog with deterministic versioning, ETags, conditional 304 responses, and catalog-specific invalidation
  • document the revised developer API in the Markdown and gated docs, with translation parity and targeted coverage

Validation

  • targeted developer API, cache, docs, and translation Vitest suites
  • scoped ESLint and Prettier checks
  • local endpoint check: 200 catalog response followed by conditional 304

npm run typecheck remains blocked by the existing unrelated error in tests/unit/spec-registry-i18n.test.ts:257.

vercel Bot and others added 7 commits July 15, 2026 11:12
…o break request loop

A server-side redirect() inside the @edit/(.)edit parallel-route interception
slot caused a runaway request loop: Next.js re-fetches the slot on every
prefetch/soft-nav, each fetch triggers fetchPendingEditId + redirect, and the
cycle repeats at ~100+ req/s from a single browser tab.

Changes:
- New EditAlreadyPendingRedirect client component that uses router.replace()
  + toast instead of server redirect()
- Updated (.)edit/page.tsx and edit/page.tsx to render the client component
  when a pending edit exists
- New EditAlreadyPendingToast on the gear page to show feedback and clean up
  the editAlreadyPending search param (mirrors EditAppliedToast pattern)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…loop

fix: replace server redirect in (.)edit with client-side navigation to break request loop
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Flohhhhh, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 675dc99a-72df-4259-bc96-62f8c490130b

📥 Commits

Reviewing files that changed from the base of the PR and between 27bae5f and a5a38c0.

📒 Files selected for processing (4)
  • src/components/layout/header-client.tsx
  • src/server/admin/gear/service.ts
  • tests/unit/admin-gear-thumbnail-service.test.ts
  • tests/unit/static-route-safety.test.ts
📝 Walkthrough

Walkthrough

Changes

The PR adds a cached, ETag-aware Developer API catalog, enriches public gear taxonomy responses, replaces implicit serialization with explicit allowlists, and updates API documentation and localizations. It also changes pending gear-edit handling from server redirects to client navigation with an informational toast.

Developer API catalog and gear contracts

Layer / File(s) Summary
Catalog data and gear composition
src/server/developer-api/constants.ts, src/server/developer-api/data.ts, src/server/developer-api/service.ts, src/server/developer-api/specs.ts
Adds catalog models and queries, cached SHA-256 snapshots, ETag helpers, and enriched mount/sensor-format gear responses.
Public payload serialization
src/server/developer-api/serializers.ts, tests/unit/developer-api-schemas-serializers.test.ts
Introduces explicit catalog and gear allowlists, nested serializers, and coverage for omitted internal fields.
Catalog route and conditional responses
src/app/api/v1/catalog/route.ts, src/server/developer-api/http.ts, tests/unit/developer-api-catalog-route.test.ts, tests/unit/developer-api-http.test.ts
Adds GET /api/v1/catalog, native 304 handling, ETags, cache headers, and response-wrapper support for native responses.
Catalog cache invalidation
src/server/developer-api/cache.ts, src/server/admin/gear/service.ts, src/server/admin/proposals/service.ts, tests/unit/*cache*, tests/unit/admin-gear-thumbnail-service.test.ts
Invalidates the catalog snapshot after relevant gear mutations and catalog-affecting proposal approvals.
Developer API documentation
docs/developer-api.md, messages/*.json, src/app/[locale]/(pages)/developer/docs/page.tsx, tests/unit/developer-docs-page.test.ts
Documents the catalog endpoint, conditional requests, taxonomy fields, and the expanded gear response examples in supported locales.

Pending gear edit navigation

Layer / File(s) Summary
Client-side pending-edit redirect and toast
src/app/[locale]/(pages)/gear/[slug]/edit/page.tsx, src/app/[locale]/(pages)/gear/[slug]/@edit/(.)edit/page.tsx, src/app/[locale]/(pages)/gear/_components/*, src/app/[locale]/(pages)/gear/[slug]/page.tsx
Replaces server redirects with client navigation and adds a toast that consumes pending-edit query parameters.

Possibly related PRs

  • Flohhhhh/sharply#365: Established the Developer API area extended here with the catalog endpoint and related service behavior.
  • Flohhhhh/sharply#359: Adds related lens image-circle handling consumed by gear taxonomy/link behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.29% 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
Title check ✅ Passed The title accurately summarizes the main change: hardening the developer gear API and adding a catalog snapshot endpoint.
Description check ✅ Passed The description matches the changeset, covering serializer hardening, the new catalog endpoint, documentation updates, and validation.
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.

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.

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sharplyphoto Ready Ready Preview, Comment Jul 15, 2026 5:02pm

Request Review

@Flohhhhh
Flohhhhh marked this pull request as ready for review July 15, 2026 16:38

@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: 5

🧹 Nitpick comments (2)
src/server/admin/gear/service.ts (1)

364-365: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider making catalog cache invalidation conditional on thumbnail changes.

setGearThumbnailService updates both thumbnailUrl and ogImageUrl. However, the developer API catalog only exposes the thumbnailUrl (as seen in CATALOG_CORE_FIELDS). If this function is called only to backfill or update the ogImageUrl while the thumbnailUrl remains unchanged, invalidating the catalog cache is unnecessary.

Depending on how this function is used (and potentially what the tests/unit/admin-gear-thumbnail-service.test.ts expects), consider tracking if the thumbnail actually changed before invalidating:

♻️ Proposed conditional invalidation
-  invalidateDeveloperApiCatalogCache();
+  if (thumbnailUrl !== currentGear.thumbnailUrl) {
+    invalidateDeveloperApiCatalogCache();
+  }
🤖 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 `@src/server/admin/gear/service.ts` around lines 364 - 365, Update
setGearThumbnailService to track whether thumbnailUrl changes independently from
ogImageUrl, and call invalidateDeveloperApiCatalogCache only when the thumbnail
value actually changes. Preserve both field updates while avoiding invalidation
for ogImageUrl-only backfills.
tests/unit/developer-api-service.test.ts (1)

29-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the cache instead of replacing it with the raw resolver.

The current mock invokes fetchCatalog on both calls, so this test only verifies deterministic hashing. Memoize the resolver and assert the catalog is fetched once.

Proposed test update
 vi.mock("next/cache", () => ({
-  unstable_cache: (resolver: () => Promise<unknown>) => resolver,
+  unstable_cache: (resolver: () => Promise<unknown>) => {
+    let cached: Promise<unknown> | undefined;
+    return () => (cached ??= resolver());
+  },
 }));

 const first = await getDeveloperCatalogSnapshot();
 const second = await getDeveloperCatalogSnapshot();
+expect(mocks.fetchCatalog).toHaveBeenCalledTimes(1);

As per coding guidelines, “Include or update automated tests covering the primary path and at least one meaningful edge case when introducing or modifying behavior.”

Also applies to: 128-162

🤖 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 `@tests/unit/developer-api-service.test.ts` around lines 29 - 31, Update the
next/cache mock around unstable_cache so it memoizes each resolver’s result
rather than returning the resolver directly. Adjust the relevant developer API
service test to invoke the cached fetch path twice and assert the catalog-fetch
operation runs only once, while preserving the existing deterministic-result
assertions.

Source: Coding guidelines

🤖 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 `@messages/de.json`:
- Around line 1896-1901: Translate the catalogTitle, catalogDescription,
catalogConditionalRequest, gearTitle, gearDescription, and gearTaxonomyNote
strings in messages/de.json lines 1896-1901 into German; make the corresponding
translations in messages/es.json lines 1896-1901 for Spanish, messages/fr.json
lines 1896-1901 for French, and messages/it.json lines 1896-1901 for Italian,
preserving the existing keys and JSON structure.

In `@src/app/`[locale]/(pages)/gear/_components/edit-already-pending-redirect.tsx:
- Around line 17-33: Update EditAlreadyPendingRedirect to use usePathname and
derive the destination by removing the current pathname’s trailing /edit
segment, then append the existing editAlreadyPending and pendingId query
parameters. Replace the hardcoded /gear/${slug} route while preserving scroll:
false and include the derived pathname in the effect dependencies.

In `@src/app/`[locale]/(pages)/gear/_components/edit-already-pending-toast.tsx:
- Around line 18-24: Update the useEffect toast.info call in the
edit-already-pending component to use translation keys for both user-facing
strings, adding matching keys and translations to every locale file under
messages/. Add a stable id to the toast options so Sonner deduplicates repeated
invocations while preserving the existing trigger condition.

In `@src/server/developer-api/cache.ts`:
- Around line 6-9: Remove the second options argument from revalidateTag in
invalidateDeveloperApiCatalogCache, leaving only
DEVELOPER_API_CATALOG_CACHE_TAG. In tests/unit/developer-api-cache.test.ts lines
10-16, update the assertion to expect revalidateTag to be called with
"developer-api-catalog" as its sole argument.

In `@tests/unit/admin-gear-thumbnail-service.test.ts`:
- Line 186: Update setGearThumbnailService to make
invalidateDeveloperApiCatalogCache conditional so the OG image backfill path
does not invalidate the catalog cache, matching the expectation in the admin
gear thumbnail test. Preserve invalidation for flows that require catalog
refresh, and keep the test assertion aligned with the resulting behavior.

---

Nitpick comments:
In `@src/server/admin/gear/service.ts`:
- Around line 364-365: Update setGearThumbnailService to track whether
thumbnailUrl changes independently from ogImageUrl, and call
invalidateDeveloperApiCatalogCache only when the thumbnail value actually
changes. Preserve both field updates while avoiding invalidation for
ogImageUrl-only backfills.

In `@tests/unit/developer-api-service.test.ts`:
- Around line 29-31: Update the next/cache mock around unstable_cache so it
memoizes each resolver’s result rather than returning the resolver directly.
Adjust the relevant developer API service test to invoke the cached fetch path
twice and assert the catalog-fetch operation runs only once, while preserving
the existing deterministic-result assertions.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d99992c6-9fbb-4bd2-9bff-7080e7cc8702

📥 Commits

Reviewing files that changed from the base of the PR and between db5485a and 27bae5f.

📒 Files selected for processing (32)
  • docs/developer-api.md
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/it.json
  • messages/ja.json
  • messages/ms.json
  • messages/zh.json
  • src/app/[locale]/(pages)/developer/docs/page.tsx
  • src/app/[locale]/(pages)/gear/[slug]/@edit/(.)edit/page.tsx
  • src/app/[locale]/(pages)/gear/[slug]/edit/page.tsx
  • src/app/[locale]/(pages)/gear/[slug]/page.tsx
  • src/app/[locale]/(pages)/gear/_components/edit-already-pending-redirect.tsx
  • src/app/[locale]/(pages)/gear/_components/edit-already-pending-toast.tsx
  • src/app/api/v1/catalog/route.ts
  • src/server/admin/gear/service.ts
  • src/server/admin/proposals/service.ts
  • src/server/developer-api/cache.ts
  • src/server/developer-api/constants.ts
  • src/server/developer-api/data.ts
  • src/server/developer-api/http.ts
  • src/server/developer-api/serializers.ts
  • src/server/developer-api/service.ts
  • src/server/developer-api/specs.ts
  • tests/unit/admin-gear-thumbnail-service.test.ts
  • tests/unit/developer-api-cache.test.ts
  • tests/unit/developer-api-catalog-route.test.ts
  • tests/unit/developer-api-http.test.ts
  • tests/unit/developer-api-schemas-serializers.test.ts
  • tests/unit/developer-api-service.test.ts
  • tests/unit/developer-docs-page.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/{app,src}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Replace any new hardcoded user-facing strings with translation keys for all locales

Files:

  • src/app/[locale]/(pages)/gear/[slug]/@edit/(.)edit/page.tsx
  • src/app/[locale]/(pages)/gear/_components/edit-already-pending-toast.tsx
  • src/server/developer-api/cache.ts
  • src/app/[locale]/(pages)/gear/[slug]/page.tsx
  • src/app/api/v1/catalog/route.ts
  • src/server/developer-api/constants.ts
  • src/app/[locale]/(pages)/gear/[slug]/edit/page.tsx
  • src/app/[locale]/(pages)/gear/_components/edit-already-pending-redirect.tsx
  • src/server/developer-api/specs.ts
  • src/app/[locale]/(pages)/developer/docs/page.tsx
  • src/server/admin/proposals/service.ts
  • src/server/developer-api/http.ts
  • src/server/developer-api/service.ts
  • src/server/admin/gear/service.ts
  • src/server/developer-api/serializers.ts
  • src/server/developer-api/data.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Include or update automated tests (vitest and/or playwright) covering the primary path and at least one meaningful edge case when introducing or modifying behavior

Files:

  • tests/unit/developer-api-cache.test.ts
  • tests/unit/developer-api-http.test.ts
  • tests/unit/developer-api-catalog-route.test.ts
  • tests/unit/developer-api-service.test.ts
  • tests/unit/developer-docs-page.test.ts
  • tests/unit/admin-gear-thumbnail-service.test.ts
  • tests/unit/developer-api-schemas-serializers.test.ts
messages/**/*.json

📄 CodeRabbit inference engine (AGENTS.md)

Maintain translation key parity: if a key is added, removed, or renamed in messages/en.json, apply the same key change across all locale files in messages/ directory

Files:

  • messages/en.json
  • messages/de.json
  • messages/it.json
  • messages/ms.json
  • messages/es.json
  • messages/ja.json
  • messages/zh.json
  • messages/fr.json
🪛 Betterleaks (1.6.1)
src/app/[locale]/(pages)/developer/docs/page.tsx

[high] 203-204: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)


[high] 236-237: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

docs/developer-api.md

[high] 62-62: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

🔇 Additional comments (20)
docs/developer-api.md (1)

36-70: LGTM!

Also applies to: 71-77

src/app/[locale]/(pages)/developer/docs/page.tsx (1)

189-245: LGTM!

Also applies to: 270-272, 287-303

tests/unit/developer-docs-page.test.ts (1)

66-86: LGTM!

Also applies to: 87-89

messages/en.json (1)

1896-1901: LGTM!

src/server/developer-api/constants.ts (1)

7-16: LGTM!

tests/unit/developer-api-catalog-route.test.ts (1)

1-78: LGTM!

tests/unit/developer-api-http.test.ts (1)

67-88: LGTM!

src/server/admin/proposals/service.ts (1)

42-63: LGTM!

tests/unit/developer-api-cache.test.ts (1)

10-16: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align test with the correct revalidateTag signature.

As flagged in cache.ts, the Next.js revalidateTag API accepts only a single argument (tag: string). Asserting that it is called with a second { expire: 0 } argument expects invalid usage that will fail typechecking once restored.

💚 Proposed fix
-  expect(revalidateTag).toHaveBeenCalledWith("developer-api-catalog", {
-    expire: 0,
-  });
+  expect(revalidateTag).toHaveBeenCalledWith("developer-api-catalog");
			> Likely an incorrect or invalid review comment.
src/server/developer-api/data.ts (1)

3-106: LGTM!

src/server/developer-api/service.ts (1)

4-130: LGTM!

Also applies to: 450-523

src/server/developer-api/specs.ts (1)

9-9: LGTM!

Also applies to: 56-56

tests/unit/developer-api-service.test.ts (1)

1-28: LGTM!

Also applies to: 33-126

src/server/developer-api/serializers.ts (1)

7-25: LGTM!

Also applies to: 35-359, 379-379, 435-474

tests/unit/developer-api-schemas-serializers.test.ts (1)

4-7: LGTM!

Also applies to: 47-100, 141-450

src/server/developer-api/http.ts (1)

18-32: LGTM!

Also applies to: 109-118

src/app/api/v1/catalog/route.ts (1)

1-27: LGTM!

src/app/[locale]/(pages)/gear/[slug]/@edit/(.)edit/page.tsx (1)

3-3: LGTM!

Also applies to: 36-44

src/app/[locale]/(pages)/gear/[slug]/edit/page.tsx (1)

4-4: LGTM!

Also applies to: 53-61

src/app/[locale]/(pages)/gear/[slug]/page.tsx (1)

63-63: LGTM!

Also applies to: 260-260

Comment thread messages/de.json
Comment thread src/server/developer-api/cache.ts
Comment thread tests/unit/admin-gear-thumbnail-service.test.ts
@Flohhhhh
Flohhhhh merged commit 6dad6a6 into main Jul 15, 2026
5 checks passed
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.

1 participant