From 21abf8768fa435a6ab19fbd1883cc85bb4a8170f Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 02:45:38 +0000 Subject: [PATCH] feat(api): GET /api/events endpoint + OpenAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Latest game events, newest first; auto-initializes like the other reads. ?limit= defaults to 50 (cap 200), non-integer/out-of-range → 400. OpenAPI spec gains the path + Event schema (14 -> 15 paths). --- app/api/events/route.ts | 21 +++++++++++++++++++ lib/openapi.test.ts | 17 ++++++++++++++++ lib/openapi.ts | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 app/api/events/route.ts diff --git a/app/api/events/route.ts b/app/api/events/route.ts new file mode 100644 index 0000000..3427066 --- /dev/null +++ b/app/api/events/route.ts @@ -0,0 +1,21 @@ +import { getGameDb } from '@/lib/db/client' +import { ensureSave } from '@/lib/game/save' +import { EventError, getEvents, parseEventLimit } from '@/lib/game/events' + +// The latest game events, newest first; auto-initializes on first hit. +export async function GET(req: Request) { + const db = await getGameDb() + await ensureSave(db) // getEvents assumes a save exists + + let limit: number + try { + limit = parseEventLimit(new URL(req.url).searchParams.get('limit')) + } catch (err) { + if (err instanceof EventError) { + return Response.json({ error: err.message }, { status: err.status }) + } + throw err + } + + return Response.json(await getEvents(db, limit)) +} diff --git a/lib/openapi.test.ts b/lib/openapi.test.ts index 1f9cb42..05c283e 100644 --- a/lib/openapi.test.ts +++ b/lib/openapi.test.ts @@ -6,6 +6,7 @@ describe('OpenAPI spec', () => { it('documents every public endpoint', () => { expect(Object.keys(openApiSpec.paths).sort()).toEqual([ '/api/crew', + '/api/events', '/api/explore', '/api/jump', '/api/missions', @@ -77,6 +78,22 @@ describe('OpenAPI spec', () => { } }); + it('describes GET /api/events → Event[] with a limit query param and a 400 error response', () => { + const events = openApiSpec.paths['/api/events'].get.responses[200].content['application/json'].schema; + expect(events.type).toBe('array'); + expect((events.items as { $ref: string }).$ref).toBe('#/components/schemas/Event'); + + const props = openApiSpec.components.schemas.Event.properties; + expect(Object.keys(props)).toEqual(['id', 'turn', 'type', 'message', 'createdAt']); + expect((props.type as { enum: string[] }).enum).toEqual(['new_game', 'jump', 'explore', 'research', 'upgrade', 'expand', 'build', 'mission']); + + const get = openApiSpec.paths['/api/events'].get; + const limit = (get.parameters ?? []).find((p) => p.name === 'limit'); + expect(limit?.in).toBe('query'); + expect(limit?.required).toBe(false); + expect(get.responses['400'].content['application/json'].schema.$ref).toBe('#/components/schemas/Error'); + }); + it('describes /api/crew as an array of CrewMember and crewFound on ExploreResult', () => { const crew = openApiSpec.paths['/api/crew'].get.responses[200].content['application/json'].schema; expect(crew.type).toBe('array'); diff --git a/lib/openapi.ts b/lib/openapi.ts index 6e01b2e..686aabb 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -523,6 +523,40 @@ export const openApiSpec = { }, }, }, + '/api/events': { + get: { + summary: 'Event log', + description: "The save's game events, newest first — every action (new game, jump, scan, research, ship work, mission) appends one. Auto-initializes a new game when no save exists yet.", + operationId: 'getEvents', + parameters: [ + { + name: 'limit', + in: 'query', + required: false, + schema: { type: 'integer', minimum: 1, maximum: 200 }, + description: 'How many of the latest events to return (default 50)', + }, + ], + responses: { + 200: { + description: 'Latest events, newest first', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: '#/components/schemas/Event' } }, + }, + }, + }, + 400: { + description: 'limit is not an integer in [1, 200]', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Error' }, + }, + }, + }, + }, + }, + }, }, components: { schemas: { @@ -682,6 +716,17 @@ export const openApiSpec = { }, required: ['id', 'type', 'description', 'targetSystemId', 'conditionKind', 'progressTarget', 'progress', 'rewardKind', 'rewardQty', 'state'], }, + Event: { + type: 'object', + properties: { + id: { type: 'integer' }, + turn: { type: 'integer', description: 'The save turn the action cost (new_game is 0)' }, + type: { type: 'string', enum: ['new_game', 'jump', 'explore', 'research', 'upgrade', 'expand', 'build', 'mission'] }, + message: { type: 'string' }, + createdAt: { type: 'string', description: 'ISO 8601 timestamp' }, + }, + required: ['id', 'turn', 'type', 'message', 'createdAt'], + }, UpgradeRequest: { type: 'object', properties: {