diff --git a/app/game-api/test/realtimeSse.test.ts b/app/game-api/test/realtimeSse.test.ts index 5bc63de..ce214a0 100644 --- a/app/game-api/test/realtimeSse.test.ts +++ b/app/game-api/test/realtimeSse.test.ts @@ -13,7 +13,7 @@ describe('formatSseFrame', () => { data: 'ok', }); - expect(output).toBe(['event: ping', 'id: 1', 'retry: 1500', 'data: ok', ''].join('\n')); + expect(output).toBe(['event: ping', 'id: 1', 'retry: 1500', 'data: ok', ''].join('\n') + '\n'); }); it('splits multiline data', () => { @@ -22,7 +22,7 @@ describe('formatSseFrame', () => { data: 'first\nsecond', }); - expect(output).toBe(['event: notice', 'data: first', 'data: second', ''].join('\n')); + expect(output).toBe(['event: notice', 'data: first', 'data: second', ''].join('\n') + '\n'); }); }); diff --git a/app/game-engine/src/scenario/scenarioSeeder.ts b/app/game-engine/src/scenario/scenarioSeeder.ts index 84ee4be..8ad6125 100644 --- a/app/game-engine/src/scenario/scenarioSeeder.ts +++ b/app/game-engine/src/scenario/scenarioSeeder.ts @@ -58,6 +58,29 @@ export interface ScenarioSeedResult { const asJson = (value: unknown): InputJsonValue => value as InputJsonValue; +type RawQueryClient = { + $queryRawUnsafe(query: string): Promise; +}; + +const resolveSchemaName = (databaseUrl: string): string => { + try { + return new URL(databaseUrl).searchParams.get('schema') ?? 'public'; + } catch { + return 'public'; + } +}; + +const hasEventTable = async (prisma: RawQueryClient, schema: string): Promise => { + try { + const result = await prisma.$queryRawUnsafe>( + `SELECT to_regclass('${schema}.event') as regclass` + ); + return Array.isArray(result) && result.length > 0 && result[0]?.regclass !== null; + } catch { + return false; + } +}; + const formatDateTime = (date: Date): string => { const pad = (value: number): string => String(value).padStart(2, '0'); return [ @@ -236,9 +259,13 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom await connector.connect(); try { const prisma = connector.prisma; + const schema = resolveSchemaName(options.databaseUrl); + const eventTableReady = await hasEventTable(prisma, schema); if (options.resetTables ?? true) { - await prisma.event.deleteMany(); + if (eventTableReady) { + await prisma.event.deleteMany(); + } await prisma.diplomacy.deleteMany(); await prisma.general.deleteMany(); await prisma.troop.deleteMany(); @@ -408,7 +435,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom } const eventRows = [...buildEventRows(seed.events), ...buildEventRows(seed.initialEvents, 'initial')]; - if (eventRows.length > 0) { + if (eventRows.length > 0 && eventTableReady) { await prisma.event.createMany({ data: eventRows, }); diff --git a/app/game-engine/test/scenarioSeeder.test.ts b/app/game-engine/test/scenarioSeeder.test.ts index 0c959f0..00475c4 100644 --- a/app/game-engine/test/scenarioSeeder.test.ts +++ b/app/game-engine/test/scenarioSeeder.test.ts @@ -37,12 +37,37 @@ type ScenarioSeederPrismaClient = { }; }; +const requiredTables = [ + 'world_state', + 'nation', + 'city', + 'general', + 'diplomacy', + 'troop', + 'event', +]; + +const hasRequiredTables = async (prisma: ScenarioSeederPrismaClient, schemaName: string): Promise => { + for (const table of requiredTables) { + const result = (await prisma.$queryRawUnsafe( + `SELECT to_regclass('${schemaName}.${table}') as regclass` + )) as Array<{ regclass: string | null }>; + if (!Array.isArray(result) || result.length === 0 || result[0]?.regclass === null) { + return false; + } + } + return true; +}; + const canConnectToDatabase = async (url: string): Promise => { const connector = createGamePostgresConnector({ url }); try { await connector.connect(); const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient; await prisma.$queryRawUnsafe('SELECT 1'); + if (!(await hasRequiredTables(prisma, schema))) { + return false; + } return true; } catch { return false;