fix(admin): environment scope for DLQ replay#76
Conversation
The dead_letters PK is (environment_id, id), so the same job id can be parked in two environments. The admin replay endpoints matched the bare id across environments and reported a hard-coded replayed: 1. Both replay endpoints now accept an optional environment slug (mirroring the CLI's --env), dlq::replay returns the true moved count, and the response reports it. Unknown slugs 404. Closes dodopayments#56
Greptile SummaryThis PR scopes admin DLQ replay by environment when requested. The main changes are:
Confidence Score: 5/5This looks safe to merge after a small optional-query cleanup.
server/src/api/admin.rs Important Files Changed
Reviews (1): Last reviewed commit: "fix(admin): environment scope for DLQ re..." | Re-trigger Greptile |
| async fn replay_scope(state: &AppState, slug: Option<String>) -> Result<Option<Uuid>, ApiError> { | ||
| let Some(slug) = slug else { return Ok(None) }; |
There was a problem hiding this comment.
When a client sends ?environment= from an empty form field or unset selector, Option<String> deserializes to Some(""). This path then looks up the empty slug and returns no such environment instead of behaving like the omitted optional scope.
| async fn replay_scope(state: &AppState, slug: Option<String>) -> Result<Option<Uuid>, ApiError> { | |
| let Some(slug) = slug else { return Ok(None) }; | |
| async fn replay_scope(state: &AppState, slug: Option<String>) -> Result<Option<Uuid>, ApiError> { | |
| let Some(slug) = slug.filter(|slug| !slug.is_empty()) else { | |
| return Ok(None); | |
| }; |
Context Used: CLAUDE.md (source)
|
I implemented this same issue independently on #80 from the same base commit, and two results from that work transfer directly here since the diffs are near-identical in shape.
schema:
type: string
nullable: trueutoipa as pinned in this repo renders an
let job_typeid = ids::typeid(ids::JOB, ids::new_uuid());With nothing parked, the asserted 404 is also produced by the The three HTTP tests in |
Review follow-ups on dodopayments#56: - The hand-written yaml added `nullable: true` to the environment query param; utoipa renders an Option<String> query field without it (like cursor/filter), so api.d.ts is regenerated to `environment?: string`. - A blank `?environment=` now means no scope instead of a 404 on the empty slug. - The unknown-environment test parks a real row and asserts it is left untouched, so it fails if scope resolution regresses to no-op.
Closes #56.
The
dead_lettersPK is(environment_id, id), so the same job id can legally be parked in two environments. The admin replay endpoints matched the bare id across environments (dlq::replay(pool, id, None)) and reported a hard-codedreplayed: 1-- a replay could move two rows while claiming one.What
POST /admin/api/dlq/{job_id}/replayandPOST /admin/api/dlq/replay-allaccept an optionalenvironmentquery parameter (a slug, mirroring the CLI's--env). Unknown slugs 404 before any replay runs.dlq::replayreturns the moved-row count (u64) instead of a bool, and the endpoint reports the true count -- an unscoped id replay that moves two rows now saysreplayed: 2.replay-all404, so the contract stays honest. The newAdminReplayQueryuses#[into_params(parameter_in = Query)]so it renders as a real query parameter. (Observation, out of scope here: the existing plainIntoParamsderives, e.g.AdminNotificationFilter, render their query fields asin: pathin the generated spec -- the utoipa default whenparameter_inis unspecified. Happy to file/fix that separately.)Tests
redteam_dlq_env_scope: the by-id lib test now asserts the exact moved count (1) with a same-id row parked in a second environment.admin.rs(HTTP plane): unscoped replay of a same-id-in-two-environments pair reportsreplayed: 2and empties both; an?environment=scoped replay moves only that environment's copy and reports 1; an unknown slug 404s.Generated artifacts
docs/openapi/chimely.yamlupdated andpackages/client/src/generated/api.d.tsregenerated from it via openapi-typescript (the same stepgenerate.shruns). My environment cannot run the Rust half ofpnpm generate(no MSVC toolchain), so the yaml hunk was derived to match utoipa's output format exactly -- worth a one-commandpnpm generatesanity pass on a machine with cargo to confirm byte-identity.cargo fmt --checkpasses; clippy/nextest verified on CI.A patch changeset covers the generated-type addition in
@chimely/client.