Skip to content

feat(api/v2): ✨ Implement claim routes on a worker queue - #136

Open
kyanvde wants to merge 1 commit into
api/v2from
api-v2/claims
Open

feat(api/v2): ✨ Implement claim routes on a worker queue#136
kyanvde wants to merge 1 commit into
api/v2from
api-v2/claims

Conversation

@kyanvde

@kyanvde kyanvde commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #61.

Fills in the claims section, and introduces the queue this API uses to hand background work to apps/worker.

Routes

Route Auth Notes
GET /claims optional Team in path or team filter; falls back to the token's team, then to every team
GET /:teamId/claims public
GET /claims/:id?external= public external=true reads the ID as the team's own externalId
POST /claims team
POST /claims/import team Bulk upsert matched on externalId
PUT /claims/:id?external= team
DELETE /claims/:id?external= team
GET /claims.geojson public
GET /:teamId/claims.geojson public
GET /claims/images team

Each is registered bare and behind a :teamId prefix, with the prefix only ever allowed to name the team the token belongs to (@TeamScope).

Worker queue

QueueService (common/queue/) adds BullMQ jobs to the EventQueue that apps/worker already consumes, so nothing slow or externally dependent runs inside a request:

Trigger Jobs
create, import BUILDTEAM_WEBHOOK (CLAIM_CREATE/CLAIM_UPDATE), SEND_DISCORD_LOG, SYNC_CLAIM_OSM
update BUILDTEAM_WEBHOOK (CLAIM_UPDATE), SEND_DISCORD_LOG, plus SYNC_CLAIM_OSM only when the outline changed
delete BUILDTEAM_WEBHOOK (CLAIM_DELETE), SEND_DISCORD_LOG

This is the part that most changes how a claim write behaves compared with v1. v1 called Overpass and Nominatim inline and made the request fail with a 500 when the building count could not be fetched. Here only the geometry that can be derived locally (area, size, center) is written in the request; buildings, osmName and city are left to SYNC_CLAIM_OSM, which is exactly what that task already computes. A create therefore answers immediately and those columns fill in shortly after.

Job names and payload shapes live in common/queue/jobs.ts and mirror the Zod schemas in apps/worker/src/tasks/ — a change to either has to be made on both. The API only needs the queue name, so no worker code is imported. bullmq and ioredis are pinned to the ranges the worker uses.

Two deliberate failure choices, both because the row is already committed by the time a job is dispatched: without REDIS_URL dispatching is a logged no-op (local dev and tests need no Redis), and a dispatch that fails is logged rather than thrown, so a queue outage never answers 500 for a write that succeeded.

Other notes

  • .geojson answers raw GeoJSON, not the { status, message, data } envelope, so the URL can be handed straight to a map client. That needed a small addition to ResponseInterceptor — a @RawResponse() decorator, which nothing else uses.
  • owner and builders take a whitelisted reference (id, ssoId, discordId or minecraft) rather than v1's free-form object. v1 passed that object to Prisma as a where, which let a caller query the user table on any column.
  • Import is an upsert, not a replace: claims the payload does not mention are left alone. Capped at 100 per request, and a payload naming the same externalId twice is a 400.
  • An externalId belonging to another team answers 404, not 403 — a 403 would confirm the ID exists. Same reasoning as the other sections.
  • GET /claims/images is scoped to the authenticated team's claims. v1's equivalent was a global moderation list gated on a per-user permission, which v2's per-team token model has no equivalent for.

Not included

DELETE /claims/:claimId/images/:imgId — the last of the two parenthesised routes in the issue. Deleting an image means deleting the S3 object, and the uploads/S3 service that does that is introduced by the showcases PR (#78, now merged into api/v2 as of writing this branch's base). It is a small follow-up on top of UploadsService.deleteIfUnreferenced; adding a second copy here would only have created a conflict. Everything else in the issue, bold and not, is implemented.

Testing

yarn ws api-v2 test — 30 suites, 234 tests, all passing. 78 are new or rewritten:

  • claims.service.spec.ts — listing, single lookup by ID and by externalId, GeoJSON assembly, image scoping, user resolution, and which jobs each mutation queues (including that an update with no new outline does not queue an OSM sync).
  • claims.routes.spec.ts — end to end through the real router: claims/images resolving ahead of claims/:id, claims/import ahead of a claim ID, the .geojson body arriving unwrapped, and the :teamId prefix rejecting another team.
  • queue.service.spec.ts — job names, the no-Redis no-op, and that a dispatch failure is reported rather than thrown.
  • util/area.spec.ts — coordinate parsing, ring closing, bounding-box centre and area.

yarn ws api-v2 build passes, and the Swagger document builds with all ten claim paths registered. yarn ws api-v2 lint reports the 6 pre-existing unbound-method errors documented in CLAUDE.md and nothing new.

🤖 Generated with Claude Code

Fills in the claims section: the paginated and GeoJSON listings, a single
claim by ID or by the ID its team uses, create, bulk import, update and
delete. Reads are public, writes are scoped to the authenticated team,
and every route exists bare and behind a :teamId prefix.

Adds QueueService, which hands slow or external work to apps/worker over
the BullMQ queue it already consumes. Reverse geocoding, building counts,
Discord messages and build team webhooks are queued rather than awaited,
so no request waits on a third party and no third party outage fails a
write that already committed.

Closes #61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

feat(api/v2): ⭐ Implement BuildTeam routes

1 participant