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
+18
View File
@@ -0,0 +1,18 @@
import { PrismaClient as GamePrismaClient } from '../prisma/generated/game/index.js';
export {
LogCategory,
LogScope,
Prisma as GamePrisma,
} from '../prisma/generated/game/index.js';
export type { PrismaClient as GamePrismaClient } from '../prisma/generated/game/index.js';
import type { PostgresConfig, PostgresConnector } from './postgres.js';
import { createPostgresConnector } from './postgres.js';
export const createGamePostgresConnector = (
config: PostgresConfig
): PostgresConnector<GamePrismaClient> =>
createPostgresConnector(
config,
(options) => new GamePrismaClient(options)
);
+19
View File
@@ -0,0 +1,19 @@
import { PrismaClient as GatewayPrismaClient } from '../prisma/generated/gateway/index.js';
export {
GatewayBuildStatus,
GatewayProfileStatus,
OAuthType,
Prisma as GatewayPrisma,
} from '../prisma/generated/gateway/index.js';
export type { PrismaClient as GatewayPrismaClient } from '../prisma/generated/gateway/index.js';
import type { PostgresConfig, PostgresConnector } from './postgres.js';
import { createPostgresConnector } from './postgres.js';
export const createGatewayPostgresConnector = (
config: PostgresConfig
): PostgresConnector<GatewayPrismaClient> =>
createPostgresConnector(
config,
(options) => new GatewayPrismaClient(options)
);
+15
View File
@@ -1,4 +1,19 @@
export * from './postgres.js';
export {
createGamePostgresConnector,
GamePrisma,
LogCategory,
LogScope,
} from './gamePrisma.js';
export type { GamePrismaClient } from './gamePrisma.js';
export {
createGatewayPostgresConnector,
GatewayBuildStatus,
GatewayProfileStatus,
GatewayPrisma,
OAuthType,
} from './gatewayPrisma.js';
export type { GatewayPrismaClient } from './gatewayPrisma.js';
export * from './db.js';
export * from './logRepository.js';
export * from './redis.js';
+6 -7
View File
@@ -1,5 +1,4 @@
import { LogCategory, LogScope } from '@prisma/client';
import type { Prisma, PrismaClient } from '@prisma/client';
import { LogCategory, LogScope, type GamePrisma, type GamePrismaClient } from './gamePrisma.js';
export interface LogQueryOptions {
limit?: number;
@@ -21,9 +20,9 @@ export interface LogEntryView {
}
const buildPaginationWhere = (
base: Prisma.LogEntryWhereInput,
base: GamePrisma.LogEntryWhereInput,
options: LogQueryOptions
): Prisma.LogEntryWhereInput => {
): GamePrisma.LogEntryWhereInput => {
if (options.beforeId) {
return {
...base,
@@ -34,16 +33,16 @@ const buildPaginationWhere = (
};
const buildFindArgs = (
where: Prisma.LogEntryWhereInput,
where: GamePrisma.LogEntryWhereInput,
options: LogQueryOptions
): Prisma.LogEntryFindManyArgs => ({
): GamePrisma.LogEntryFindManyArgs => ({
where: buildPaginationWhere(where, options),
orderBy: { id: 'desc' },
take: options.limit ?? 50,
});
export class LogRepository {
constructor(private readonly prisma: PrismaClient) {}
constructor(private readonly prisma: GamePrismaClient) {}
// 전역(시스템) 로그 조회
async listSystemLogs(
+74 -12
View File
@@ -1,4 +1,3 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
@@ -22,21 +21,75 @@ export interface PostgresConnector<TClient = unknown> {
disconnect(): Promise<void>;
}
export interface PrismaClientFactoryOptions {
adapter: PrismaPg;
log?: PostgresLogOption[];
}
export type PrismaClientFactory<TClient> = (
options: PrismaClientFactoryOptions
) => TClient;
const resolveSchemaName = (
value: string | undefined
): string => {
if (!value) {
return 'public';
}
const trimmed = value.trim();
return trimmed ? trimmed : 'public';
};
const applySchemaToDatabaseUrl = (
url: string,
schema: string | undefined
): string => {
if (!schema) {
return url;
}
try {
const parsed = new URL(url);
parsed.searchParams.set('schema', resolveSchemaName(schema));
return parsed.toString();
} catch {
return url;
}
};
const extractSchemaFromDatabaseUrl = (
url: string
): string | undefined => {
try {
const parsed = new URL(url);
const schema = parsed.searchParams.get('schema');
return schema && schema.trim() ? schema.trim() : undefined;
} catch {
return undefined;
}
};
const buildDatabaseUrlFromEnv = (
env: NodeJS.ProcessEnv
env: NodeJS.ProcessEnv,
schemaOverride?: string
): string => {
const host = env.POSTGRES_HOST ?? '127.0.0.1';
const port = env.POSTGRES_PORT ?? '15432';
const user = env.POSTGRES_USER ?? 'sammo';
const password = env.POSTGRES_PASSWORD ?? '';
const dbName = env.POSTGRES_DB ?? 'sammo';
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
const schema = resolveSchemaName(
schemaOverride ?? env.POSTGRES_SCHEMA ?? env.DATABASE_SCHEMA
);
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=${schema}`;
};
export const resolvePostgresConfigFromEnv = (
env: NodeJS.ProcessEnv = process.env
options: { env?: NodeJS.ProcessEnv; schema?: string } = {}
): PostgresConfig => {
const url = env.DATABASE_URL ?? buildDatabaseUrlFromEnv(env);
const env = options.env ?? process.env;
const url = env.DATABASE_URL
? applySchemaToDatabaseUrl(env.DATABASE_URL, options.schema)
: buildDatabaseUrlFromEnv(env, options.schema);
if (!url) {
throw new Error('DATABASE_URL is required to create a Postgres client.');
}
@@ -44,23 +97,32 @@ export const resolvePostgresConfigFromEnv = (
return { url };
};
export const createPostgresConnector = (
config: PostgresConfig
): PostgresConnector => {
export const createPostgresConnector = <TClient>(
config: PostgresConfig,
createClient: PrismaClientFactory<TClient>
): PostgresConnector<TClient> => {
const schema =
extractSchemaFromDatabaseUrl(config.url) ??
process.env.POSTGRES_SCHEMA ??
process.env.DATABASE_SCHEMA;
const pool = new Pool({
connectionString: config.url,
...(schema ? { options: `-c search_path=${schema}` } : {}),
});
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({
const adapter = new PrismaPg(
pool,
schema ? { schema } : undefined
);
const prisma = createClient({
adapter,
log: config.log,
});
return {
prisma,
connect: () => prisma.$connect(),
connect: () => (prisma as { $connect: () => Promise<void> }).$connect(),
disconnect: async () => {
await prisma.$disconnect();
await (prisma as { $disconnect: () => Promise<void> }).$disconnect();
await pool.end();
},
};