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.
This commit is contained in:
2026-01-03 14:47:28 +00:00
parent 67d995c65f
commit a9609dcc19
33 changed files with 388 additions and 158 deletions
@@ -1,4 +1,4 @@
import { Prisma, type PrismaClient } from '@prisma/client';
import { GatewayPrisma, type GatewayPrismaClient } from '@sammo-ts/infra';
import { createSimplePasswordHasher, type PasswordHasher } from './passwordHasher.js';
import type {
@@ -29,12 +29,12 @@ const mapUser = (row: {
displayName: string;
passwordHash: string;
passwordSalt: string;
roles: Prisma.JsonValue;
sanctions: Prisma.JsonValue;
roles: GatewayPrisma.JsonValue;
sanctions: GatewayPrisma.JsonValue;
oauthType: 'NONE' | 'KAKAO';
oauthId: string | null;
email: string | null;
oauthInfo: Prisma.JsonValue;
oauthInfo: GatewayPrisma.JsonValue;
createdAt: Date;
}): UserRecord => ({
id: row.id,
@@ -52,7 +52,7 @@ const mapUser = (row: {
});
export const createPostgresUserRepository = (
prisma: PrismaClient,
prisma: GatewayPrismaClient,
hasher: PasswordHasher = createSimplePasswordHasher()
): UserRepository => {
return {
@@ -98,12 +98,12 @@ export const createPostgresUserRepository = (
displayName: input.displayName ?? input.username,
passwordHash: hasher.hash(input.password, salt),
passwordSalt: salt,
roles: ['user'] satisfies Prisma.JsonArray,
sanctions: {} satisfies Prisma.JsonObject,
roles: ['user'] satisfies GatewayPrisma.JsonArray,
sanctions: {} satisfies GatewayPrisma.JsonObject,
oauthType,
oauthId: input.oauth?.id,
email: input.oauth?.email?.toLowerCase(),
oauthInfo: (input.oauth?.info ?? {}) as Prisma.JsonObject,
oauthInfo: (input.oauth?.info ?? {}) as GatewayPrisma.JsonObject,
},
});
return mapUser(row);
@@ -125,7 +125,7 @@ export const createPostgresUserRepository = (
await prisma.appUser.update({
where: { id: userId },
data: {
oauthInfo: oauthInfo as Prisma.JsonObject,
oauthInfo: oauthInfo as GatewayPrisma.JsonObject,
},
});
},
+12
View File
@@ -4,6 +4,7 @@ export interface GatewayApiConfig {
host: string;
port: number;
trpcPath: string;
dbSchema: string;
redisKeyPrefix: string;
flushChannel: string;
sessionTtlSeconds: number;
@@ -24,6 +25,7 @@ export interface GatewayApiConfig {
}
export interface GatewayOrchestratorConfig {
dbSchema: string;
redisKeyPrefix: string;
gameTokenSecret: string;
orchestratorReconcileIntervalMs: number;
@@ -58,6 +60,14 @@ const parseBoolean = (value: string | undefined, fallback: boolean): boolean =>
return fallback;
};
const resolveSchemaName = (value: string | undefined): string => {
if (!value) {
return 'public';
}
const trimmed = value.trim();
return trimmed ? trimmed : 'public';
};
export const resolveGatewayApiConfigFromEnv = (
env: NodeJS.ProcessEnv = process.env
): GatewayApiConfig => {
@@ -76,6 +86,7 @@ export const resolveGatewayApiConfigFromEnv = (
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
port: parseNumber(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
trpcPath: env.TRPC_PATH ?? '/trpc',
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
redisKeyPrefix,
flushChannel: `${redisKeyPrefix}:flush`,
sessionTtlSeconds: parseNumber(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
@@ -127,6 +138,7 @@ export const resolveGatewayOrchestratorConfigFromEnv = (
}
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
return {
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
redisKeyPrefix,
gameTokenSecret: secret,
orchestratorReconcileIntervalMs: parseNumber(
+3 -3
View File
@@ -6,7 +6,7 @@ import type { OAuthSessionStore } from './auth/oauthSessionStore.js';
import type { GatewayProfileRepository } from './orchestrator/profileRepository.js';
import type { GatewayOrchestratorHandle } from './orchestrator/gatewayOrchestrator.js';
import type { GatewayProfileStatusService } from './lobby/profileStatusService.js';
import type { PrismaClient } from '@prisma/client';
import type { GatewayPrismaClient } from '@sammo-ts/infra';
export interface GatewayApiContext {
users: UserRepository;
@@ -22,7 +22,7 @@ export interface GatewayApiContext {
profileStatus: GatewayProfileStatusService;
adminToken?: string;
requestHeaders: Record<string, string | string[] | undefined>;
prisma: PrismaClient;
prisma: GatewayPrismaClient;
}
export const createGatewayApiContext = (options: {
@@ -39,7 +39,7 @@ export const createGatewayApiContext = (options: {
profileStatus: GatewayProfileStatusService;
adminToken?: string;
requestHeaders?: Record<string, string | string[] | undefined>;
prisma: PrismaClient;
prisma: GatewayPrismaClient;
}): GatewayApiContext => ({
users: options.users,
sessions: options.sessions,
@@ -1,4 +1,4 @@
import type { PrismaClient } from '@prisma/client';
import type { GatewayPrismaClient } from '@sammo-ts/infra';
import type { GatewayOrchestratorConfig } from '../config.js';
import { createGatewayProfileRepository } from './profileRepository.js';
@@ -16,7 +16,7 @@ export const buildEnvMap = (env: NodeJS.ProcessEnv): Record<string, string> => {
};
export const createGatewayOrchestrator = (
prisma: PrismaClient,
prisma: GatewayPrismaClient,
config: GatewayOrchestratorConfig,
env: NodeJS.ProcessEnv = process.env
): {
@@ -1,6 +1,6 @@
import type { PrismaClient } from '@prisma/client';
import {
createPostgresConnector,
createGatewayPostgresConnector,
type GatewayPrismaClient,
resolvePostgresConfigFromEnv,
} from '@sammo-ts/infra';
@@ -9,11 +9,13 @@ import { createGatewayOrchestrator } from './orchestratorFactory.js';
export const runGatewayOrchestrator = async (): Promise<void> => {
const config = resolveGatewayOrchestratorConfigFromEnv();
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
const postgres = createGatewayPostgresConnector(
resolvePostgresConfigFromEnv({ schema: config.dbSchema })
);
await postgres.connect();
const { orchestrator } = createGatewayOrchestrator(
postgres.prisma as PrismaClient,
postgres.prisma as GatewayPrismaClient,
config,
process.env
);
@@ -1,4 +1,4 @@
import { Prisma, type PrismaClient } from '@prisma/client';
import { GatewayPrisma, type GatewayPrismaClient } from '@sammo-ts/infra';
export const GATEWAY_PROFILE_STATUSES = [
'RESERVED',
@@ -38,7 +38,7 @@ export interface GatewayProfileRecord {
buildCompletedAt?: string;
buildError?: string;
lastError?: string;
meta: Prisma.JsonObject;
meta: GatewayPrisma.JsonObject;
createdAt: string;
updatedAt: string;
}
@@ -52,7 +52,7 @@ export interface GatewayProfileUpsertInput {
openAt?: string;
scheduledStartAt?: string;
buildCommitSha?: string;
meta?: Prisma.JsonObject;
meta?: GatewayPrisma.JsonObject;
}
export interface GatewayProfileRepository {
@@ -113,7 +113,7 @@ type GatewayProfileRow = {
buildCompletedAt: Date | null;
buildError: string | null;
lastError: string | null;
meta: Prisma.JsonValue;
meta: GatewayPrisma.JsonValue;
createdAt: Date;
updatedAt: Date;
};
@@ -145,7 +145,7 @@ const mapProfile = (row: GatewayProfileRow): GatewayProfileRecord => ({
buildCompletedAt: toIso(row.buildCompletedAt),
buildError: row.buildError ?? undefined,
lastError: row.lastError ?? undefined,
meta: (row.meta ?? {}) as Prisma.JsonObject,
meta: (row.meta ?? {}) as GatewayPrisma.JsonObject,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
});
@@ -154,7 +154,7 @@ const buildProfileName = (profile: string, scenario: string): string =>
`${profile}:${scenario}`;
export const createGatewayProfileRepository = (
prisma: PrismaClient
prisma: GatewayPrismaClient
): GatewayProfileRepository => ({
async listProfiles(): Promise<GatewayProfileRecord[]> {
const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient;
@@ -187,7 +187,7 @@ export const createGatewayProfileRepository = (
? new Date(input.scheduledStartAt)
: null,
buildCommitSha: input.buildCommitSha ?? null,
meta: (input.meta ?? {}) as Prisma.JsonObject,
meta: (input.meta ?? {}) as GatewayPrisma.JsonObject,
},
update: {
apiPort: input.apiPort,
@@ -211,7 +211,7 @@ export const createGatewayProfileRepository = (
input.buildCommitSha === undefined
? undefined
: input.buildCommitSha,
meta: input.meta ? (input.meta as Prisma.JsonObject) : undefined,
meta: input.meta ? (input.meta as GatewayPrisma.JsonObject) : undefined,
},
});
return mapProfile(row);
+8 -6
View File
@@ -1,10 +1,10 @@
import fastify, { type FastifyRequest } from 'fastify';
import cors from '@fastify/cors';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import type { PrismaClient } from '@prisma/client';
import {
createPostgresConnector,
createGatewayPostgresConnector,
createRedisConnector,
type GatewayPrismaClient,
resolvePostgresConfigFromEnv,
resolveRedisConfigFromEnv,
} from '@sammo-ts/infra';
@@ -22,13 +22,15 @@ import { RepositoryProfileStatusService } from './lobby/profileStatusService.js'
export const createGatewayApiServer = async () => {
const config = resolveGatewayApiConfigFromEnv();
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
const postgres = createGatewayPostgresConnector(
resolvePostgresConfigFromEnv({ schema: config.dbSchema })
);
const redis = createRedisConnector(resolveRedisConfigFromEnv());
await postgres.connect();
await redis.connect();
const users = createPostgresUserRepository(
postgres.prisma as PrismaClient
postgres.prisma as GatewayPrismaClient
);
const sessions = new RedisGatewaySessionService(redis.client, {
keyPrefix: config.redisKeyPrefix,
@@ -48,7 +50,7 @@ export const createGatewayApiServer = async () => {
);
const { orchestrator, profiles } = createGatewayOrchestrator(
postgres.prisma as PrismaClient,
postgres.prisma as GatewayPrismaClient,
config,
process.env
);
@@ -82,7 +84,7 @@ export const createGatewayApiServer = async () => {
profileStatus,
adminToken: config.adminToken,
requestHeaders: req.headers,
prisma: postgres.prisma as PrismaClient,
prisma: postgres.prisma as GatewayPrismaClient,
}),
},
});