Files
core2026/app/game-api/src/server.ts
T
Hide_D a9609dcc19 feat: add database schema support for gateway and game profiles
- Introduced `dbSchema` configuration option in GatewayApiConfig and GatewayOrchestratorConfig.
- Implemented schema resolution logic in environment configuration functions.
- Updated context and orchestrator factory to use GatewayPrismaClient instead of PrismaClient.
- Refactored orchestrator server and profile repository to accommodate new database schema handling.
- Created separate Prisma schemas for game and gateway in the infra package.
- Enhanced Postgres connector to support schema overrides in database URLs.
- Updated documentation to reflect changes in database schema handling.
- Added new Prisma generation and database push scripts for game and gateway schemas.
2026-01-03 14:47:28 +00:00

128 lines
4.1 KiB
TypeScript

import fastify, { type FastifyRequest } from 'fastify';
import cors from '@fastify/cors';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import {
createGamePostgresConnector,
createRedisConnector,
resolvePostgresConfigFromEnv,
resolveRedisConfigFromEnv,
} from '@sammo-ts/infra';
import { resolveGameApiConfigFromEnv } from './config.js';
import { createGameApiContext, type DatabaseClient } from './context.js';
import { buildTurnDaemonStreamKeys } from './daemon/streamKeys.js';
import { RedisTurnDaemonTransport } from './daemon/redisTransport.js';
import { InMemoryFlushStore, RedisGatewayFlushSubscriber } from './auth/flushStore.js';
import { createGameTokenVerifier } from './auth/tokenVerifier.js';
import { appRouter } from './router.js';
import { buildBattleSimQueueKeys } from './battleSim/keys.js';
import { RedisBattleSimTransport } from './battleSim/redisTransport.js';
const extractBearerToken = (value: string | string[] | undefined): string | null => {
if (!value) {
return null;
}
const header = Array.isArray(value) ? value[0] : value;
if (!header) {
return null;
}
const prefix = 'Bearer ';
if (header.startsWith(prefix)) {
return header.slice(prefix.length).trim();
}
return header.trim();
};
export const createGameApiServer = async () => {
const config = resolveGameApiConfigFromEnv();
const postgres = createGamePostgresConnector(
resolvePostgresConfigFromEnv({ schema: config.profile })
);
const redis = createRedisConnector(resolveRedisConfigFromEnv());
await postgres.connect();
await redis.connect();
const turnDaemon = new RedisTurnDaemonTransport(redis.client, {
keys: buildTurnDaemonStreamKeys(config.profileName),
requestTimeoutMs: config.daemonRequestTimeoutMs,
});
const battleSim = new RedisBattleSimTransport(redis.client, {
keys: buildBattleSimQueueKeys(config.profileName),
requestTimeoutMs: config.battleSimRequestTimeoutMs,
resultTtlSeconds: config.battleSimResultTtlSeconds,
});
const flushStore = new InMemoryFlushStore();
const flushSubscriberClient = redis.client.duplicate();
await flushSubscriberClient.connect();
const flushSubscriber = new RedisGatewayFlushSubscriber(
flushSubscriberClient,
config.flushChannel,
flushStore
);
await flushSubscriber.start();
const tokenVerifier = createGameTokenVerifier({
secret: config.gameTokenSecret,
profileName: config.profileName,
flushStore,
});
const app = fastify({
logger: true,
});
await app.register(cors, {
origin: true,
credentials: true,
});
await app.register(fastifyTRPCPlugin, {
prefix: config.trpcPath,
trpcOptions: {
router: appRouter,
createContext: ({ req }: { req: FastifyRequest }) => {
const token = extractBearerToken(req.headers.authorization);
const auth = token ? tokenVerifier.verify(token) : null;
return createGameApiContext({
db: postgres.prisma as unknown as DatabaseClient,
redis: redis.client,
turnDaemon,
battleSim,
profile: {
id: config.profile,
scenario: config.scenario,
name: config.profileName,
},
auth,
});
},
},
});
app.get('/healthz', async () => ({
ok: true,
profile: config.profileName,
}));
app.addHook('onClose', async () => {
await flushSubscriber.stop();
await flushSubscriberClient.quit();
await redis.disconnect();
await postgres.disconnect();
});
return {
app,
config,
};
};
export const runGameApiServer = async (): Promise<void> => {
const { app, config } = await createGameApiServer();
await app.listen({
host: config.host,
port: config.port,
});
};