From 1d8399f64a08b381deb89703d145498d40118b2b Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Tue, 25 Aug 2026 12:41:57 -0400 Subject: [PATCH] [events] fix event delete --- apps/api/src/events-db.ts | 19 +++++++++++++++++ apps/api/src/routes/events.ts | 25 +++++++++++++---------- apps/api/src/test/integration/api.test.ts | 9 +++----- pnpm-lock.yaml | 9 ++++++++ 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/apps/api/src/events-db.ts b/apps/api/src/events-db.ts index ff6d30c..0eee8ba 100644 --- a/apps/api/src/events-db.ts +++ b/apps/api/src/events-db.ts @@ -212,6 +212,25 @@ export function toEventResult(event: PlayerEvent, tags: EventTag[] = []): Player } } +/** + * The envelope a DELETE answers with. Both payload fields are null: the reference reports + * only that the delete happened, and the client reads nothing but `Result` — there is no + * event left to redraw. Deliberately NOT {@link toEventResult}'s shape, even though both + * are the v2 envelope. + */ +export interface PlayerEventDeletedResult { + PlayerEvent: null + Result: number + TagModifyResult: null +} + +/** The one value {@link PlayerEventDeletedResult} ever takes: a successful delete. */ +export const EVENT_DELETED_RESULT: PlayerEventDeletedResult = { + PlayerEvent: null, + Result: 0, + TagModifyResult: null, +} + /** * The projection of an event carried on a hub notification frame (`PlayerEventCreated` * and its siblings). Deliberately NOT the stored record, in three ways — don't unify diff --git a/apps/api/src/routes/events.ts b/apps/api/src/routes/events.ts index f065991..38e6594 100644 --- a/apps/api/src/routes/events.ts +++ b/apps/api/src/routes/events.ts @@ -10,6 +10,7 @@ import { NotificationType } from '../../../notify/src/notification-types' import { createEvent, deleteEvent, + EVENT_DELETED_RESULT, eventInputRejection, getEventAttendees, getEventById, @@ -45,6 +46,7 @@ import { PlayerEventAccessibilityRequest, PlayerEventBaseDto, PlayerEventBulkInviteRequest, + PlayerEventDeletedDto, PlayerEventDescriptionRequest, PlayerEventDetailsDto, PlayerEventDto, @@ -693,8 +695,9 @@ export const eventRoutes = new Hono({ strict: false }) // which is how the reference exposes it, and a client that reaches for the HTTP verb // instead should not get a 404 for being right. // - // Answers the v2 envelope carrying the event as it WAS, so the caller can report what it - // removed; an unknown event is 404, and someone else's is 403. + // Answers the v2 envelope with both payload fields nulled — + // `{ PlayerEvent: null, Result: 0, TagModifyResult: null }`, which is what the reference + // sends: there is nothing left to redraw. An unknown event is 404, and someone else's 403. .on( ['POST', 'DELETE'], '/api/playerevents/v2/delete/:eventId{[0-9]+}', @@ -706,12 +709,14 @@ export const eventRoutes = new Hono({ strict: false }) 'whose attendee rows outlived it would still be counted, and its tags would still ' + 'answer `#tag` searches.\n\n' + 'Creator only: anyone else gets 403, and an unknown event 404. Answers the v2 ' + - 'envelope carrying the event as it was just before it went. Both POST and DELETE ' + - 'reach it — the path names the verb, which is the form the client uses.', + 'envelope with `PlayerEvent` and `TagModifyResult` both null — the event is gone, ' + + 'so there is nothing for the client to redraw from, and it reads only `Result`. ' + + 'Both POST and DELETE reach it — the path names the verb, which is the form the ' + + 'client uses.', security: AUTHED, parameters: [idParam('eventId', 'Event id')], responses: { - 200: json(PlayerEventResultDto, 'The event that was deleted'), + 200: json(PlayerEventDeletedDto, 'The nulled envelope a delete answers with'), 401: UNAUTHORIZED_RESPONSE, 403: { description: 'Not the event’s creator (empty body)' }, 404: { description: 'No such event (empty body)' }, @@ -726,12 +731,10 @@ export const eventRoutes = new Hono({ strict: false }) if (existing === null) return c.body(null, 404) if (existing.CreatorPlayerId !== id) return c.body(null, 403) - // Read the tags before the delete takes them, so the envelope can still report what - // the event carried. - const tags = await getEventTags(c.env.DB, eventId) - const deleted = await deleteEvent(c.env.DB, eventId) - // deleteEvent only answers null when the row vanished, which the read above rules out. - return c.json(toEventResult(deleted!, tags)) + await deleteEvent(c.env.DB, eventId) + // Both payload fields are null here — the delete envelope is not the one the other + // v2 routes answer with. Nothing is left to redraw, and the client reads `Result`. + return c.json(EVENT_DELETED_RESULT) } ) diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index 05ce8de..9593a54 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -4603,12 +4603,9 @@ describe('player events', () => { const res = await post(`/api/playerevents/v2/delete/${eventId}`, {}) expect(res.status).toBe(200) - // The envelope carries the event as it was, tags included — the caller can report - // what it removed. - const body = (await res.json()) as PlayerEventResult - expect(body.Result).toBe(0) - expect(body.PlayerEvent.PlayerEventId).toBe(eventId) - expect(body.PlayerEvent.Tags).toEqual(['meetup']) + // Both payload fields are null: the event is gone, so the envelope reports only that + // the delete succeeded. NOT the shape the other v2 routes answer with. + expect(await res.json()).toEqual({ PlayerEvent: null, Result: 0, TagModifyResult: null }) // Gone, and nothing left hanging off it: orphan RSVPs would keep being counted and // orphan tags would keep answering `#tag` searches. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba1b46e..8c57230 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -758,9 +758,18 @@ importers: hono: specifier: 4.12.27 version: 4.12.27 + hono-openapi: + specifier: 1.3.1 + version: 1.3.1(@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.27))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.4.3))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod@4.4.3))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.4.3))(@types/json-schema@7.0.15)(hono@4.12.27)(openapi-types@12.1.3) + openapi-types: + specifier: 12.1.3 + version: 12.1.3 workers-tagged-logger: specifier: 1.0.1 version: 1.0.1 + zod: + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@cloudflare/vitest-pool-workers': specifier: 0.16.20