feat(vps): port admin to HttpApiBuilder.group (Step 6)#163
Merged
Conversation
Owner
Author
|
Fixed: extracted the shared |
guidefari
added a commit
that referenced
this pull request
Jul 11, 2026
Addresses review comment: dieOnDatabaseError was an identical copy-paste across search/music/profile/resolve handlers, differing only in the log-tag prefix string. Extracted to apps/vps/src/http/handler-utils.ts as a tag -> wrapper factory; this PR updates resolve.handlers.ts to use it. The other three handler files' duplicate copies are updated on migration/6-admin-group (PR #163), which also introduces the shared helper -- once both branches merge into migration/effect-http-api, all four groups share one definition.
Ports GET /api/admin/overview, /api/admin/frontend-errors/:scenario, and
GET /api/admin/newsletter-subscribers from the Hono/zod-openapi router
onto the Effect router. All three are admin-gated via .middleware(AuthMiddleware)
plus an in-handler role check (requireAdmin), matching the pattern already
used for music-artists' write endpoints.
effect@4.0.0-beta.93's HttpApiError has no built-in 429 -- the frontend
error simulator (a QA tool exercising www's error boundary/Sentry-reporting
threshold against every real status code) needs one, so
SimulatedRateLimitError is declared in packages/api/src/admin.ts the same
way Phase 2 of the migration doc ports any other domain error. The other
simulated-error branches use the built-in HttpApiError classes directly
(bodyless) rather than preserving the old handler's { error, scenario }
response body -- confirmed via apps/www/src/lib/http-client.ts that the
frontend only branches on res.status, not response body shape.
getAdminOverview's ~30-query aggregation batch has no dedicated service
layer (same as the old Hono handler); ported as one Effect.tryPromise
wrapping the whole batch, matching the old handler's single try/catch
around one Promise.all rather than converting each query individually.
Deletes the superseded routes/admin/{admin.routes,admin.index,admin.handlers}.ts
Hono trio and its app.ts mount. No apps/www changes -- fetcher() calls the
same /api/admin/* paths unchanged, verified end-to-end against the real
prod-connected dev server (admin overview dashboard + newsletter tab both
render real data through the new handler, logged in via a local dev
account).
Review on #162 flagged dieOnDatabaseError as a likely duplicate -- correct: search, music, profile, and this PR's admin handlers each had an identical copy differing only in the log-tag prefix string. Extracted to apps/vps/src/http/handler-utils.ts as a tag -> wrapper factory, with each handler file partially applying its own tag once at module scope so call sites stay unchanged (dieOnDatabaseError(effect)).
d879976 to
e2c405e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Step 6 continues: port
admin(GET /api/admin/overview,GET /api/admin/frontend-errors/:scenario,GET /api/admin/newsletter-subscribers)from the Hono fallback onto the Effect router.
What to look for
packages/api/src/admin.ts-- effect@4.0.0-beta.93'sHttpApiErrorhas nobuilt-in 429. The frontend error simulator (
apps/www/src/routes/admin/frontend-errors.tsx,a QA tool exercising www's error boundary and the
res.status >= 500Sentry-reporting threshold in
http-client.tsagainst every real statuscode) needs one, so
SimulatedRateLimitErroris declared the same wayPhase 2 of the migration doc ports any other domain error
(
Schema.TaggedErrorClass+httpApiStatus: 429).bad-request/not-found/error/unavailable)use the built-in
HttpApiErrorclasses directly, which are bodyless, ratherthan preserving the old Hono handler's
{ error, scenario }response body.Confirmed via
apps/www/src/lib/http-client.tsthat the frontend onlybranches on
res.status, not the response body shape, so this is a safesimplification, not a silent behavior loss.
.middleware(AuthMiddleware)plus anin-handler
requireAdminrole check (apps/vps/src/http/admin.handlers.ts),matching the pattern already used for music-artists' write endpoints
(
music.handlers.ts'srequireAdmin).AuthMiddlewarerunsbefore param schema validation, so an invalid
:scenarioliteral still401s (not 400) when there's no session cookie -- verified empirically via a
new blackbox test, not assumed from endpoint declaration order (my first
draft of that test asserted 400 and was wrong).
getAdminOverview's ~30-query aggregation batch has no dedicated servicelayer, same as the old Hono handler -- ported as one
Effect.tryPromisewrapping the whole batch (
loadAdminOverview()), matching the oldhandler's single try/catch around one
Promise.allrather than convertingeach query to its own tagged-error Effect, which would be new
failure-isolation behavior, not a faithful port.
apps/wwwchanges --fetcher()calls the same/api/admin/*pathsunchanged; this PR only swaps which server-side code answers them.
apps/vps/src/db/admin-overview.schema.ts(the old Zod schema) isintentionally not deleted, unlike the other superseded Hono files --
apps/www/src/routes/admin/-overview.data.tsstill imports itsAdminOverview/AdminOverviewContentBreakdowntypes from@gbfm/vps/schemasforuseAdminOverview's return type. Nothing inapps/vpsimports it anymore (admin.handlers.ts'sloadAdminOverview()was written directly against the real query results, independently of this
schema, and matches
packages/api/src/admin.ts'sAdminOverviewResponsefield-for-field). It's now a type-only shim for
apps/www, not a runtimeschema -- worth retiring once a step-6b PR swaps
useAdminOverviewontothe typed client and can import the type from
packages/api/src/admin.tsinstead. Flagging this explicitly rather than leaving unexplained
leftover-looking code: an earlier adversarial review pass on this PR
flagged the file as fully dead and safe to delete, which was wrong --
it missed this type-only cross-package import.
Test evidence
apps/www's admin overview dashboard and newsletter tab both consume theseendpoints directly (
useAdminOverview,useAdminNewsletterSubscribersinapps/www/src/lib/http.ts/-overview.data.ts), so this isn'tscreenshot-exempt. Verified against a real dev server connected to the
actual prod database (read-only GETs only), logged in as a real admin
account:
Browser console clean after login (no errors from either page beyond the
expected pre-login 401 on the initial unauthenticated load).
bun run typecheck(vps and api packages): clean.vitest run --config vitest.unit.config.ts(vps): added 4 new blackboxcases (401 for all three endpoints without a session cookie, plus the
auth-before-param-validation ordering case above) -- all 4 pass. Pre-existing
DB-connectivity failures (7, same as unmodified
migration/effect-http-api:health/music-artists/search/profile blackbox tests failing 500 instead of
200/404 because the local testcontainers DB isn't reachable in this
environment) are unchanged by this PR.