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:
+3
-1
@@ -1,18 +1,20 @@
|
|||||||
POSTGRES_DB=sammo_test
|
POSTGRES_DB=sammo_test
|
||||||
POSTGRES_USER=sammo
|
POSTGRES_USER=sammo
|
||||||
POSTGRES_PASSWORD=ci-postgres
|
POSTGRES_PASSWORD=ci-postgres
|
||||||
|
POSTGRES_SCHEMA=public
|
||||||
REDIS_PASSWORD=ci-redis
|
REDIS_PASSWORD=ci-redis
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PORT=16379
|
REDIS_PORT=16379
|
||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
GAME_TOKEN_SECRET=ci-secret
|
GAME_TOKEN_SECRET=ci-secret
|
||||||
GATEWAY_REDIS_PREFIX=sammo:gateway
|
GATEWAY_REDIS_PREFIX=sammo:gateway
|
||||||
|
GATEWAY_DB_SCHEMA=public
|
||||||
GATEWAY_API_HOST=127.0.0.1
|
GATEWAY_API_HOST=127.0.0.1
|
||||||
GATEWAY_API_PORT=13000
|
GATEWAY_API_PORT=13000
|
||||||
GATEWAY_PUBLIC_URL=http://localhost:13000
|
GATEWAY_PUBLIC_URL=http://localhost:13000
|
||||||
GAME_API_HOST=127.0.0.1
|
GAME_API_HOST=127.0.0.1
|
||||||
GAME_API_PORT=14000
|
GAME_API_PORT=14000
|
||||||
PROFILE=che
|
PROFILE=hwe
|
||||||
SCENARIO=default
|
SCENARIO=default
|
||||||
KAKAO_REST_KEY=ci-kakao-rest-key
|
KAKAO_REST_KEY=ci-kakao-rest-key
|
||||||
KAKAO_REDIRECT_URI=http://localhost:13000/oauth/kakao/callback
|
KAKAO_REDIRECT_URI=http://localhost:13000/oauth/kakao/callback
|
||||||
|
|||||||
+4
-1
@@ -4,7 +4,9 @@ POSTGRES_PORT=15432
|
|||||||
POSTGRES_DB=sammo
|
POSTGRES_DB=sammo
|
||||||
POSTGRES_USER=sammo
|
POSTGRES_USER=sammo
|
||||||
POSTGRES_PASSWORD=change-me
|
POSTGRES_PASSWORD=change-me
|
||||||
|
# POSTGRES_SCHEMA=public
|
||||||
# DATABASE_URL=postgresql://sammo:change-me@127.0.0.1:15432/sammo?schema=public
|
# DATABASE_URL=postgresql://sammo:change-me@127.0.0.1:15432/sammo?schema=public
|
||||||
|
# GATEWAY_DATABASE_URL=postgresql://sammo:change-me@127.0.0.1:15432/sammo?schema=public
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
@@ -21,6 +23,7 @@ GATEWAY_API_HOST=0.0.0.0
|
|||||||
GATEWAY_API_PORT=13000
|
GATEWAY_API_PORT=13000
|
||||||
GATEWAY_PUBLIC_URL=http://localhost:13000
|
GATEWAY_PUBLIC_URL=http://localhost:13000
|
||||||
GATEWAY_REDIS_PREFIX=sammo:gateway
|
GATEWAY_REDIS_PREFIX=sammo:gateway
|
||||||
|
GATEWAY_DB_SCHEMA=public
|
||||||
SESSION_TTL_SECONDS=604800
|
SESSION_TTL_SECONDS=604800
|
||||||
GAME_SESSION_TTL_SECONDS=21600
|
GAME_SESSION_TTL_SECONDS=21600
|
||||||
OAUTH_SESSION_TTL_SECONDS=600
|
OAUTH_SESSION_TTL_SECONDS=600
|
||||||
@@ -31,7 +34,7 @@ KAKAO_REDIRECT_URI=http://localhost:13000/oauth/kakao/callback
|
|||||||
# Game API
|
# Game API
|
||||||
GAME_API_HOST=0.0.0.0
|
GAME_API_HOST=0.0.0.0
|
||||||
GAME_API_PORT=14000
|
GAME_API_PORT=14000
|
||||||
PROFILE=che
|
PROFILE=hwe
|
||||||
SCENARIO=default
|
SCENARIO=default
|
||||||
DAEMON_REQUEST_TIMEOUT_MS=5000
|
DAEMON_REQUEST_TIMEOUT_MS=5000
|
||||||
TRPC_PATH=/trpc
|
TRPC_PATH=/trpc
|
||||||
|
|||||||
+3
-1
@@ -1,18 +1,20 @@
|
|||||||
POSTGRES_DB=sammo
|
POSTGRES_DB=sammo
|
||||||
POSTGRES_USER=sammo
|
POSTGRES_USER=sammo
|
||||||
POSTGRES_PASSWORD=change-me
|
POSTGRES_PASSWORD=change-me
|
||||||
|
POSTGRES_SCHEMA=public
|
||||||
REDIS_PASSWORD=change-me
|
REDIS_PASSWORD=change-me
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PORT=16379
|
REDIS_PORT=16379
|
||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
GAME_TOKEN_SECRET=change-me
|
GAME_TOKEN_SECRET=change-me
|
||||||
GATEWAY_REDIS_PREFIX=sammo:gateway
|
GATEWAY_REDIS_PREFIX=sammo:gateway
|
||||||
|
GATEWAY_DB_SCHEMA=public
|
||||||
GATEWAY_API_HOST=0.0.0.0
|
GATEWAY_API_HOST=0.0.0.0
|
||||||
GATEWAY_API_PORT=13000
|
GATEWAY_API_PORT=13000
|
||||||
GATEWAY_PUBLIC_URL=https://example.com
|
GATEWAY_PUBLIC_URL=https://example.com
|
||||||
GAME_API_HOST=0.0.0.0
|
GAME_API_HOST=0.0.0.0
|
||||||
GAME_API_PORT=14000
|
GAME_API_PORT=14000
|
||||||
PROFILE=che
|
PROFILE=hwe
|
||||||
SCENARIO=default
|
SCENARIO=default
|
||||||
KAKAO_REST_KEY=your-kakao-rest-key
|
KAKAO_REST_KEY=your-kakao-rest-key
|
||||||
KAKAO_ADMIN_KEY=your-kakao-admin-key
|
KAKAO_ADMIN_KEY=your-kakao-admin-key
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ out
|
|||||||
# Nuxt.js build / generate output
|
# Nuxt.js build / generate output
|
||||||
.nuxt
|
.nuxt
|
||||||
dist
|
dist
|
||||||
|
packages/infra/prisma/generated/
|
||||||
|
|
||||||
# Gatsby files
|
# Gatsby files
|
||||||
.cache/
|
.cache/
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const parseNumber = (value: string | undefined, fallback: number, label: string)
|
|||||||
export const resolveGameApiConfigFromEnv = (
|
export const resolveGameApiConfigFromEnv = (
|
||||||
env: NodeJS.ProcessEnv = process.env
|
env: NodeJS.ProcessEnv = process.env
|
||||||
): GameApiConfig => {
|
): GameApiConfig => {
|
||||||
const profile = env.PROFILE ?? env.SERVER_PROFILE ?? 'che';
|
const profile = env.PROFILE ?? env.SERVER_PROFILE ?? 'hwe';
|
||||||
const scenario = env.SCENARIO ?? 'default';
|
const scenario = env.SCENARIO ?? 'default';
|
||||||
const profileName = `${profile}:${scenario}`;
|
const profileName = `${profile}:${scenario}`;
|
||||||
const secret = env.GAME_TOKEN_SECRET ?? env.GATEWAY_TOKEN_SECRET ?? '';
|
const secret = env.GAME_TOKEN_SECRET ?? env.GATEWAY_TOKEN_SECRET ?? '';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import fastify, { type FastifyRequest } from 'fastify';
|
|||||||
import cors from '@fastify/cors';
|
import cors from '@fastify/cors';
|
||||||
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGamePostgresConnector,
|
||||||
createRedisConnector,
|
createRedisConnector,
|
||||||
resolvePostgresConfigFromEnv,
|
resolvePostgresConfigFromEnv,
|
||||||
resolveRedisConfigFromEnv,
|
resolveRedisConfigFromEnv,
|
||||||
@@ -35,7 +35,9 @@ const extractBearerToken = (value: string | string[] | undefined): string | null
|
|||||||
|
|
||||||
export const createGameApiServer = async () => {
|
export const createGameApiServer = async () => {
|
||||||
const config = resolveGameApiConfigFromEnv();
|
const config = resolveGameApiConfigFromEnv();
|
||||||
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
|
const postgres = createGamePostgresConnector(
|
||||||
|
resolvePostgresConfigFromEnv({ schema: config.profile })
|
||||||
|
);
|
||||||
const redis = createRedisConnector(resolveRedisConfigFromEnv());
|
const redis = createRedisConnector(resolveRedisConfigFromEnv());
|
||||||
|
|
||||||
await postgres.connect();
|
await postgres.connect();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type EnvMap = Record<string, string | undefined>;
|
|||||||
export interface DatabaseUrlOptions {
|
export interface DatabaseUrlOptions {
|
||||||
envFile?: string;
|
envFile?: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
|
schema?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseEnvFile = (rawText: string): EnvMap => {
|
const parseEnvFile = (rawText: string): EnvMap => {
|
||||||
@@ -47,18 +48,42 @@ const loadEnvFile = async (envFile: string): Promise<EnvMap> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applySchemaToDatabaseUrl = (
|
||||||
|
url: string,
|
||||||
|
schema: string | undefined
|
||||||
|
): string => {
|
||||||
|
if (!schema) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
parsed.searchParams.set('schema', schema);
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const resolveDatabaseUrl = async (
|
export const resolveDatabaseUrl = async (
|
||||||
options?: DatabaseUrlOptions
|
options?: DatabaseUrlOptions
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const env = options?.env ?? process.env;
|
const env = options?.env ?? process.env;
|
||||||
if (env.DATABASE_URL) {
|
if (env.DATABASE_URL) {
|
||||||
return env.DATABASE_URL;
|
const schema =
|
||||||
|
options?.schema ??
|
||||||
|
env.POSTGRES_SCHEMA ??
|
||||||
|
env.DATABASE_SCHEMA;
|
||||||
|
return applySchemaToDatabaseUrl(env.DATABASE_URL, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFile = options?.envFile ?? DEFAULT_ENV_FILE;
|
const envFile = options?.envFile ?? DEFAULT_ENV_FILE;
|
||||||
const fileEnv = await loadEnvFile(envFile);
|
const fileEnv = await loadEnvFile(envFile);
|
||||||
if (fileEnv.DATABASE_URL) {
|
if (fileEnv.DATABASE_URL) {
|
||||||
return fileEnv.DATABASE_URL;
|
const schema =
|
||||||
|
options?.schema ??
|
||||||
|
fileEnv.POSTGRES_SCHEMA ??
|
||||||
|
fileEnv.DATABASE_SCHEMA;
|
||||||
|
return applySchemaToDatabaseUrl(fileEnv.DATABASE_URL, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = env.POSTGRES_HOST ?? fileEnv.POSTGRES_HOST ?? '127.0.0.1';
|
const host = env.POSTGRES_HOST ?? fileEnv.POSTGRES_HOST ?? '127.0.0.1';
|
||||||
@@ -66,5 +91,12 @@ export const resolveDatabaseUrl = async (
|
|||||||
const user = env.POSTGRES_USER ?? fileEnv.POSTGRES_USER ?? 'sammo';
|
const user = env.POSTGRES_USER ?? fileEnv.POSTGRES_USER ?? 'sammo';
|
||||||
const password = env.POSTGRES_PASSWORD ?? fileEnv.POSTGRES_PASSWORD ?? '';
|
const password = env.POSTGRES_PASSWORD ?? fileEnv.POSTGRES_PASSWORD ?? '';
|
||||||
const dbName = env.POSTGRES_DB ?? fileEnv.POSTGRES_DB ?? 'sammo';
|
const dbName = env.POSTGRES_DB ?? fileEnv.POSTGRES_DB ?? 'sammo';
|
||||||
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
|
const schema =
|
||||||
|
options?.schema ??
|
||||||
|
env.POSTGRES_SCHEMA ??
|
||||||
|
fileEnv.POSTGRES_SCHEMA ??
|
||||||
|
env.DATABASE_SCHEMA ??
|
||||||
|
fileEnv.DATABASE_SCHEMA ??
|
||||||
|
'public';
|
||||||
|
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=${schema}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGamePostgresConnector,
|
||||||
type InputJsonValue,
|
type InputJsonValue,
|
||||||
type TurnEngineDatabaseClient,
|
type TurnEngineDatabaseClient,
|
||||||
type TurnEngineEventCreateManyInput,
|
type TurnEngineEventCreateManyInput,
|
||||||
@@ -117,7 +117,7 @@ export const seedScenarioToDatabase = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const connector = createPostgresConnector({ url: options.databaseUrl });
|
const connector = createGamePostgresConnector({ url: options.databaseUrl });
|
||||||
const now = options.now ?? new Date();
|
const now = options.now ?? new Date();
|
||||||
const tickSeconds = options.tickSeconds ?? DEFAULT_TICK_SECONDS;
|
const tickSeconds = options.tickSeconds ?? DEFAULT_TICK_SECONDS;
|
||||||
const generalGold = options.defaultGeneralGold ?? DEFAULT_GENERAL_GOLD;
|
const generalGold = options.defaultGeneralGold ?? DEFAULT_GENERAL_GOLD;
|
||||||
@@ -125,7 +125,7 @@ export const seedScenarioToDatabase = async (
|
|||||||
|
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
try {
|
try {
|
||||||
const prisma = connector.prisma as TurnEngineDatabaseClient;
|
const prisma = connector.prisma as unknown as TurnEngineDatabaseClient;
|
||||||
|
|
||||||
if (options.resetTables ?? true) {
|
if (options.resetTables ?? true) {
|
||||||
await prisma.event.deleteMany();
|
await prisma.event.deleteMany();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface TurnDaemonCliOptions {
|
|||||||
profileName?: string;
|
profileName?: string;
|
||||||
scenario?: string;
|
scenario?: string;
|
||||||
databaseUrl?: string;
|
databaseUrl?: string;
|
||||||
|
gatewayDatabaseUrl?: string;
|
||||||
tickMinutes?: number;
|
tickMinutes?: number;
|
||||||
schedule?: TurnSchedule;
|
schedule?: TurnSchedule;
|
||||||
budget?: Partial<TurnRunBudget>;
|
budget?: Partial<TurnRunBudget>;
|
||||||
@@ -72,14 +73,22 @@ export const runTurnDaemonCli = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const env = options.env ?? process.env;
|
const env = options.env ?? process.env;
|
||||||
const profile =
|
const profile =
|
||||||
options.profile ?? env.TURN_PROFILE ?? env.PROFILE ?? 'che';
|
options.profile ?? env.TURN_PROFILE ?? env.PROFILE ?? 'hwe';
|
||||||
const scenario = options.scenario ?? env.TURN_SCENARIO ?? env.SCENARIO;
|
const scenario = options.scenario ?? env.TURN_SCENARIO ?? env.SCENARIO;
|
||||||
const profileName =
|
const profileName =
|
||||||
options.profileName ??
|
options.profileName ??
|
||||||
env.TURN_PROFILE_NAME ??
|
env.TURN_PROFILE_NAME ??
|
||||||
(scenario ? `${profile}:${scenario}` : profile);
|
(scenario ? `${profile}:${scenario}` : profile);
|
||||||
const databaseUrl =
|
const databaseUrl =
|
||||||
options.databaseUrl ?? (await resolveDatabaseUrl({ env }));
|
options.databaseUrl ??
|
||||||
|
(await resolveDatabaseUrl({ env, schema: profile }));
|
||||||
|
const gatewayDatabaseUrl =
|
||||||
|
options.gatewayDatabaseUrl ??
|
||||||
|
env.GATEWAY_DATABASE_URL ??
|
||||||
|
(await resolveDatabaseUrl({
|
||||||
|
env,
|
||||||
|
schema: env.GATEWAY_DB_SCHEMA ?? 'public',
|
||||||
|
}));
|
||||||
const budget = buildBudgetOverride(env, options.budget);
|
const budget = buildBudgetOverride(env, options.budget);
|
||||||
const tickMinutes =
|
const tickMinutes =
|
||||||
options.tickMinutes ?? parseNumber(env.TURN_TICK_MINUTES);
|
options.tickMinutes ?? parseNumber(env.TURN_TICK_MINUTES);
|
||||||
@@ -93,6 +102,7 @@ export const runTurnDaemonCli = async (
|
|||||||
profile,
|
profile,
|
||||||
profileName,
|
profileName,
|
||||||
databaseUrl,
|
databaseUrl,
|
||||||
|
gatewayDatabaseUrl,
|
||||||
defaultBudget: budget,
|
defaultBudget: budget,
|
||||||
tickMinutes,
|
tickMinutes,
|
||||||
schedule: options.schedule,
|
schedule: options.schedule,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGamePostgresConnector,
|
||||||
type InputJsonValue,
|
type InputJsonValue,
|
||||||
type TurnEngineCityUpdateInput,
|
type TurnEngineCityUpdateInput,
|
||||||
type TurnEngineDatabaseClient,
|
type TurnEngineDatabaseClient,
|
||||||
@@ -236,9 +236,9 @@ export const createDatabaseTurnHooks = async (
|
|||||||
options?: { reservedTurns?: InMemoryReservedTurnStore }
|
options?: { reservedTurns?: InMemoryReservedTurnStore }
|
||||||
): Promise<DatabaseTurnHooks> => {
|
): Promise<DatabaseTurnHooks> => {
|
||||||
// 턴 처리 결과를 DB에 반영하는 훅을 만든다.
|
// 턴 처리 결과를 DB에 반영하는 훅을 만든다.
|
||||||
const connector = createPostgresConnector({ url: databaseUrl });
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
const prisma = connector.prisma as TurnEngineDatabaseClient;
|
const prisma = connector.prisma as unknown as TurnEngineDatabaseClient;
|
||||||
|
|
||||||
const hooks: TurnDaemonHooks = {
|
const hooks: TurnDaemonHooks = {
|
||||||
flushChanges: async () => {
|
flushChanges: async () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createPostgresConnector } from '@sammo-ts/infra';
|
import { createGatewayPostgresConnector } from '@sammo-ts/infra';
|
||||||
|
|
||||||
export interface GatewayProfileGateOptions {
|
export interface GatewayProfileGateOptions {
|
||||||
databaseUrl: string;
|
databaseUrl: string;
|
||||||
|
gatewayDatabaseUrl?: string;
|
||||||
profileName: string;
|
profileName: string;
|
||||||
cacheMs?: number;
|
cacheMs?: number;
|
||||||
}
|
}
|
||||||
@@ -29,7 +30,9 @@ type GatewayProfileClient = {
|
|||||||
export const createGatewayProfileGate = async (
|
export const createGatewayProfileGate = async (
|
||||||
options: GatewayProfileGateOptions
|
options: GatewayProfileGateOptions
|
||||||
): Promise<GatewayProfileGate> => {
|
): Promise<GatewayProfileGate> => {
|
||||||
const connector = createPostgresConnector({ url: options.databaseUrl });
|
const connector = createGatewayPostgresConnector({
|
||||||
|
url: options.gatewayDatabaseUrl ?? options.databaseUrl,
|
||||||
|
});
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
const prisma = connector.prisma as unknown as {
|
const prisma = connector.prisma as unknown as {
|
||||||
gatewayProfile: GatewayProfileClient;
|
gatewayProfile: GatewayProfileClient;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGamePostgresConnector,
|
||||||
type InputJsonValue,
|
type InputJsonValue,
|
||||||
type TurnEngineDatabaseClient,
|
type TurnEngineDatabaseClient,
|
||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
@@ -269,10 +269,10 @@ export class InMemoryReservedTurnStore {
|
|||||||
export const createReservedTurnStore = async (
|
export const createReservedTurnStore = async (
|
||||||
options: ReservedTurnStoreOptions
|
options: ReservedTurnStoreOptions
|
||||||
): Promise<ReservedTurnStoreHandle> => {
|
): Promise<ReservedTurnStoreHandle> => {
|
||||||
const connector = createPostgresConnector({ url: options.databaseUrl });
|
const connector = createGamePostgresConnector({ url: options.databaseUrl });
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
const store = new InMemoryReservedTurnStore(
|
const store = new InMemoryReservedTurnStore(
|
||||||
connector.prisma as ReservedTurnDatabaseClient,
|
connector.prisma as unknown as ReservedTurnDatabaseClient,
|
||||||
{
|
{
|
||||||
maxGeneralTurns: options.maxGeneralTurns ?? DEFAULT_GENERAL_TURNS,
|
maxGeneralTurns: options.maxGeneralTurns ?? DEFAULT_GENERAL_TURNS,
|
||||||
maxNationTurns: options.maxNationTurns ?? DEFAULT_NATION_TURNS,
|
maxNationTurns: options.maxNationTurns ?? DEFAULT_NATION_TURNS,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface TurnDaemonRuntimeOptions {
|
|||||||
profile: string;
|
profile: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
databaseUrl: string;
|
databaseUrl: string;
|
||||||
|
gatewayDatabaseUrl?: string;
|
||||||
defaultBudget?: TurnRunBudget;
|
defaultBudget?: TurnRunBudget;
|
||||||
clock?: Clock;
|
clock?: Clock;
|
||||||
controlQueue?: TurnDaemonControlQueue;
|
controlQueue?: TurnDaemonControlQueue;
|
||||||
@@ -137,6 +138,7 @@ export const createTurnDaemonRuntime = async (
|
|||||||
options.profileName
|
options.profileName
|
||||||
? await createGatewayProfileGate({
|
? await createGatewayProfileGate({
|
||||||
databaseUrl: options.databaseUrl,
|
databaseUrl: options.databaseUrl,
|
||||||
|
gatewayDatabaseUrl: options.gatewayDatabaseUrl,
|
||||||
profileName: options.profileName,
|
profileName: options.profileName,
|
||||||
cacheMs: options.pauseGateIntervalMs,
|
cacheMs: options.pauseGateIntervalMs,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGamePostgresConnector,
|
||||||
type JsonValue,
|
type JsonValue,
|
||||||
type TurnEngineCityRow,
|
type TurnEngineCityRow,
|
||||||
type TurnEngineDatabaseClient,
|
type TurnEngineDatabaseClient,
|
||||||
@@ -252,10 +252,10 @@ const mapTroopRow = (row: TurnEngineTroopRow): Troop => ({
|
|||||||
export const loadTurnWorldFromDatabase = async (
|
export const loadTurnWorldFromDatabase = async (
|
||||||
options: TurnWorldLoaderOptions
|
options: TurnWorldLoaderOptions
|
||||||
): Promise<TurnWorldLoadResult> => {
|
): Promise<TurnWorldLoadResult> => {
|
||||||
const connector = createPostgresConnector({ url: options.databaseUrl });
|
const connector = createGamePostgresConnector({ url: options.databaseUrl });
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
try {
|
try {
|
||||||
const prisma = connector.prisma as TurnEngineDatabaseClient;
|
const prisma = connector.prisma as unknown as TurnEngineDatabaseClient;
|
||||||
const worldState = await prisma.worldState.findFirst();
|
const worldState = await prisma.worldState.findFirst();
|
||||||
if (!worldState) {
|
if (!worldState) {
|
||||||
throw new Error('world_state row is required to start turn daemon.');
|
throw new Error('world_state row is required to start turn daemon.');
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { createPostgresConnector } from '@sammo-ts/infra';
|
import { createGamePostgresConnector } from '@sammo-ts/infra';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import { resolveDatabaseUrl } from '../src/scenario/databaseUrl.js';
|
import { resolveDatabaseUrl } from '../src/scenario/databaseUrl.js';
|
||||||
import { seedScenarioToDatabase } from '../src/scenario/scenarioSeeder.js';
|
import { seedScenarioToDatabase } from '../src/scenario/scenarioSeeder.js';
|
||||||
|
|
||||||
const scenarioId = 1010;
|
const scenarioId = 1010;
|
||||||
const databaseUrl = await resolveDatabaseUrl();
|
const schema = process.env.POSTGRES_SCHEMA ?? 'public';
|
||||||
|
process.env.POSTGRES_SCHEMA = schema;
|
||||||
|
const databaseUrl = await resolveDatabaseUrl({ schema });
|
||||||
|
|
||||||
type ScenarioSeederPrismaClient = {
|
type ScenarioSeederPrismaClient = {
|
||||||
$queryRawUnsafe(query: string): Promise<unknown>;
|
$queryRawUnsafe(query: string): Promise<unknown>;
|
||||||
@@ -26,7 +28,7 @@ type ScenarioSeederPrismaClient = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canConnectToDatabase = async (url: string): Promise<boolean> => {
|
const canConnectToDatabase = async (url: string): Promise<boolean> => {
|
||||||
const connector = createPostgresConnector({ url });
|
const connector = createGamePostgresConnector({ url });
|
||||||
try {
|
try {
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
const prisma = connector.prisma as ScenarioSeederPrismaClient;
|
const prisma = connector.prisma as ScenarioSeederPrismaClient;
|
||||||
@@ -49,7 +51,7 @@ describeDb('scenario database seed', () => {
|
|||||||
databaseUrl,
|
databaseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connector = createPostgresConnector({ url: databaseUrl });
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
||||||
await connector.connect();
|
await connector.connect();
|
||||||
try {
|
try {
|
||||||
const prisma = connector.prisma as ScenarioSeederPrismaClient;
|
const prisma = connector.prisma as ScenarioSeederPrismaClient;
|
||||||
|
|||||||
@@ -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 { createSimplePasswordHasher, type PasswordHasher } from './passwordHasher.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -29,12 +29,12 @@ const mapUser = (row: {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
passwordHash: string;
|
passwordHash: string;
|
||||||
passwordSalt: string;
|
passwordSalt: string;
|
||||||
roles: Prisma.JsonValue;
|
roles: GatewayPrisma.JsonValue;
|
||||||
sanctions: Prisma.JsonValue;
|
sanctions: GatewayPrisma.JsonValue;
|
||||||
oauthType: 'NONE' | 'KAKAO';
|
oauthType: 'NONE' | 'KAKAO';
|
||||||
oauthId: string | null;
|
oauthId: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
oauthInfo: Prisma.JsonValue;
|
oauthInfo: GatewayPrisma.JsonValue;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}): UserRecord => ({
|
}): UserRecord => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -52,7 +52,7 @@ const mapUser = (row: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const createPostgresUserRepository = (
|
export const createPostgresUserRepository = (
|
||||||
prisma: PrismaClient,
|
prisma: GatewayPrismaClient,
|
||||||
hasher: PasswordHasher = createSimplePasswordHasher()
|
hasher: PasswordHasher = createSimplePasswordHasher()
|
||||||
): UserRepository => {
|
): UserRepository => {
|
||||||
return {
|
return {
|
||||||
@@ -98,12 +98,12 @@ export const createPostgresUserRepository = (
|
|||||||
displayName: input.displayName ?? input.username,
|
displayName: input.displayName ?? input.username,
|
||||||
passwordHash: hasher.hash(input.password, salt),
|
passwordHash: hasher.hash(input.password, salt),
|
||||||
passwordSalt: salt,
|
passwordSalt: salt,
|
||||||
roles: ['user'] satisfies Prisma.JsonArray,
|
roles: ['user'] satisfies GatewayPrisma.JsonArray,
|
||||||
sanctions: {} satisfies Prisma.JsonObject,
|
sanctions: {} satisfies GatewayPrisma.JsonObject,
|
||||||
oauthType,
|
oauthType,
|
||||||
oauthId: input.oauth?.id,
|
oauthId: input.oauth?.id,
|
||||||
email: input.oauth?.email?.toLowerCase(),
|
email: input.oauth?.email?.toLowerCase(),
|
||||||
oauthInfo: (input.oauth?.info ?? {}) as Prisma.JsonObject,
|
oauthInfo: (input.oauth?.info ?? {}) as GatewayPrisma.JsonObject,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return mapUser(row);
|
return mapUser(row);
|
||||||
@@ -125,7 +125,7 @@ export const createPostgresUserRepository = (
|
|||||||
await prisma.appUser.update({
|
await prisma.appUser.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: {
|
data: {
|
||||||
oauthInfo: oauthInfo as Prisma.JsonObject,
|
oauthInfo: oauthInfo as GatewayPrisma.JsonObject,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface GatewayApiConfig {
|
|||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
trpcPath: string;
|
trpcPath: string;
|
||||||
|
dbSchema: string;
|
||||||
redisKeyPrefix: string;
|
redisKeyPrefix: string;
|
||||||
flushChannel: string;
|
flushChannel: string;
|
||||||
sessionTtlSeconds: number;
|
sessionTtlSeconds: number;
|
||||||
@@ -24,6 +25,7 @@ export interface GatewayApiConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayOrchestratorConfig {
|
export interface GatewayOrchestratorConfig {
|
||||||
|
dbSchema: string;
|
||||||
redisKeyPrefix: string;
|
redisKeyPrefix: string;
|
||||||
gameTokenSecret: string;
|
gameTokenSecret: string;
|
||||||
orchestratorReconcileIntervalMs: number;
|
orchestratorReconcileIntervalMs: number;
|
||||||
@@ -58,6 +60,14 @@ const parseBoolean = (value: string | undefined, fallback: boolean): boolean =>
|
|||||||
return fallback;
|
return fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveSchemaName = (value: string | undefined): string => {
|
||||||
|
if (!value) {
|
||||||
|
return 'public';
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : 'public';
|
||||||
|
};
|
||||||
|
|
||||||
export const resolveGatewayApiConfigFromEnv = (
|
export const resolveGatewayApiConfigFromEnv = (
|
||||||
env: NodeJS.ProcessEnv = process.env
|
env: NodeJS.ProcessEnv = process.env
|
||||||
): GatewayApiConfig => {
|
): GatewayApiConfig => {
|
||||||
@@ -76,6 +86,7 @@ export const resolveGatewayApiConfigFromEnv = (
|
|||||||
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
|
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
|
||||||
port: parseNumber(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
|
port: parseNumber(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
|
||||||
trpcPath: env.TRPC_PATH ?? '/trpc',
|
trpcPath: env.TRPC_PATH ?? '/trpc',
|
||||||
|
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
||||||
redisKeyPrefix,
|
redisKeyPrefix,
|
||||||
flushChannel: `${redisKeyPrefix}:flush`,
|
flushChannel: `${redisKeyPrefix}:flush`,
|
||||||
sessionTtlSeconds: parseNumber(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
|
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';
|
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
|
||||||
return {
|
return {
|
||||||
|
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
||||||
redisKeyPrefix,
|
redisKeyPrefix,
|
||||||
gameTokenSecret: secret,
|
gameTokenSecret: secret,
|
||||||
orchestratorReconcileIntervalMs: parseNumber(
|
orchestratorReconcileIntervalMs: parseNumber(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { OAuthSessionStore } from './auth/oauthSessionStore.js';
|
|||||||
import type { GatewayProfileRepository } from './orchestrator/profileRepository.js';
|
import type { GatewayProfileRepository } from './orchestrator/profileRepository.js';
|
||||||
import type { GatewayOrchestratorHandle } from './orchestrator/gatewayOrchestrator.js';
|
import type { GatewayOrchestratorHandle } from './orchestrator/gatewayOrchestrator.js';
|
||||||
import type { GatewayProfileStatusService } from './lobby/profileStatusService.js';
|
import type { GatewayProfileStatusService } from './lobby/profileStatusService.js';
|
||||||
import type { PrismaClient } from '@prisma/client';
|
import type { GatewayPrismaClient } from '@sammo-ts/infra';
|
||||||
|
|
||||||
export interface GatewayApiContext {
|
export interface GatewayApiContext {
|
||||||
users: UserRepository;
|
users: UserRepository;
|
||||||
@@ -22,7 +22,7 @@ export interface GatewayApiContext {
|
|||||||
profileStatus: GatewayProfileStatusService;
|
profileStatus: GatewayProfileStatusService;
|
||||||
adminToken?: string;
|
adminToken?: string;
|
||||||
requestHeaders: Record<string, string | string[] | undefined>;
|
requestHeaders: Record<string, string | string[] | undefined>;
|
||||||
prisma: PrismaClient;
|
prisma: GatewayPrismaClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createGatewayApiContext = (options: {
|
export const createGatewayApiContext = (options: {
|
||||||
@@ -39,7 +39,7 @@ export const createGatewayApiContext = (options: {
|
|||||||
profileStatus: GatewayProfileStatusService;
|
profileStatus: GatewayProfileStatusService;
|
||||||
adminToken?: string;
|
adminToken?: string;
|
||||||
requestHeaders?: Record<string, string | string[] | undefined>;
|
requestHeaders?: Record<string, string | string[] | undefined>;
|
||||||
prisma: PrismaClient;
|
prisma: GatewayPrismaClient;
|
||||||
}): GatewayApiContext => ({
|
}): GatewayApiContext => ({
|
||||||
users: options.users,
|
users: options.users,
|
||||||
sessions: options.sessions,
|
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 type { GatewayOrchestratorConfig } from '../config.js';
|
||||||
import { createGatewayProfileRepository } from './profileRepository.js';
|
import { createGatewayProfileRepository } from './profileRepository.js';
|
||||||
@@ -16,7 +16,7 @@ export const buildEnvMap = (env: NodeJS.ProcessEnv): Record<string, string> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createGatewayOrchestrator = (
|
export const createGatewayOrchestrator = (
|
||||||
prisma: PrismaClient,
|
prisma: GatewayPrismaClient,
|
||||||
config: GatewayOrchestratorConfig,
|
config: GatewayOrchestratorConfig,
|
||||||
env: NodeJS.ProcessEnv = process.env
|
env: NodeJS.ProcessEnv = process.env
|
||||||
): {
|
): {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PrismaClient } from '@prisma/client';
|
|
||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGatewayPostgresConnector,
|
||||||
|
type GatewayPrismaClient,
|
||||||
resolvePostgresConfigFromEnv,
|
resolvePostgresConfigFromEnv,
|
||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
|
|
||||||
@@ -9,11 +9,13 @@ import { createGatewayOrchestrator } from './orchestratorFactory.js';
|
|||||||
|
|
||||||
export const runGatewayOrchestrator = async (): Promise<void> => {
|
export const runGatewayOrchestrator = async (): Promise<void> => {
|
||||||
const config = resolveGatewayOrchestratorConfigFromEnv();
|
const config = resolveGatewayOrchestratorConfigFromEnv();
|
||||||
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
|
const postgres = createGatewayPostgresConnector(
|
||||||
|
resolvePostgresConfigFromEnv({ schema: config.dbSchema })
|
||||||
|
);
|
||||||
await postgres.connect();
|
await postgres.connect();
|
||||||
|
|
||||||
const { orchestrator } = createGatewayOrchestrator(
|
const { orchestrator } = createGatewayOrchestrator(
|
||||||
postgres.prisma as PrismaClient,
|
postgres.prisma as GatewayPrismaClient,
|
||||||
config,
|
config,
|
||||||
process.env
|
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 = [
|
export const GATEWAY_PROFILE_STATUSES = [
|
||||||
'RESERVED',
|
'RESERVED',
|
||||||
@@ -38,7 +38,7 @@ export interface GatewayProfileRecord {
|
|||||||
buildCompletedAt?: string;
|
buildCompletedAt?: string;
|
||||||
buildError?: string;
|
buildError?: string;
|
||||||
lastError?: string;
|
lastError?: string;
|
||||||
meta: Prisma.JsonObject;
|
meta: GatewayPrisma.JsonObject;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export interface GatewayProfileUpsertInput {
|
|||||||
openAt?: string;
|
openAt?: string;
|
||||||
scheduledStartAt?: string;
|
scheduledStartAt?: string;
|
||||||
buildCommitSha?: string;
|
buildCommitSha?: string;
|
||||||
meta?: Prisma.JsonObject;
|
meta?: GatewayPrisma.JsonObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayProfileRepository {
|
export interface GatewayProfileRepository {
|
||||||
@@ -113,7 +113,7 @@ type GatewayProfileRow = {
|
|||||||
buildCompletedAt: Date | null;
|
buildCompletedAt: Date | null;
|
||||||
buildError: string | null;
|
buildError: string | null;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
meta: Prisma.JsonValue;
|
meta: GatewayPrisma.JsonValue;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
@@ -145,7 +145,7 @@ const mapProfile = (row: GatewayProfileRow): GatewayProfileRecord => ({
|
|||||||
buildCompletedAt: toIso(row.buildCompletedAt),
|
buildCompletedAt: toIso(row.buildCompletedAt),
|
||||||
buildError: row.buildError ?? undefined,
|
buildError: row.buildError ?? undefined,
|
||||||
lastError: row.lastError ?? undefined,
|
lastError: row.lastError ?? undefined,
|
||||||
meta: (row.meta ?? {}) as Prisma.JsonObject,
|
meta: (row.meta ?? {}) as GatewayPrisma.JsonObject,
|
||||||
createdAt: row.createdAt.toISOString(),
|
createdAt: row.createdAt.toISOString(),
|
||||||
updatedAt: row.updatedAt.toISOString(),
|
updatedAt: row.updatedAt.toISOString(),
|
||||||
});
|
});
|
||||||
@@ -154,7 +154,7 @@ const buildProfileName = (profile: string, scenario: string): string =>
|
|||||||
`${profile}:${scenario}`;
|
`${profile}:${scenario}`;
|
||||||
|
|
||||||
export const createGatewayProfileRepository = (
|
export const createGatewayProfileRepository = (
|
||||||
prisma: PrismaClient
|
prisma: GatewayPrismaClient
|
||||||
): GatewayProfileRepository => ({
|
): GatewayProfileRepository => ({
|
||||||
async listProfiles(): Promise<GatewayProfileRecord[]> {
|
async listProfiles(): Promise<GatewayProfileRecord[]> {
|
||||||
const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient;
|
const gatewayProfile = prisma.gatewayProfile as unknown as GatewayProfileClient;
|
||||||
@@ -187,7 +187,7 @@ export const createGatewayProfileRepository = (
|
|||||||
? new Date(input.scheduledStartAt)
|
? new Date(input.scheduledStartAt)
|
||||||
: null,
|
: null,
|
||||||
buildCommitSha: input.buildCommitSha ?? null,
|
buildCommitSha: input.buildCommitSha ?? null,
|
||||||
meta: (input.meta ?? {}) as Prisma.JsonObject,
|
meta: (input.meta ?? {}) as GatewayPrisma.JsonObject,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
apiPort: input.apiPort,
|
apiPort: input.apiPort,
|
||||||
@@ -211,7 +211,7 @@ export const createGatewayProfileRepository = (
|
|||||||
input.buildCommitSha === undefined
|
input.buildCommitSha === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: input.buildCommitSha,
|
: input.buildCommitSha,
|
||||||
meta: input.meta ? (input.meta as Prisma.JsonObject) : undefined,
|
meta: input.meta ? (input.meta as GatewayPrisma.JsonObject) : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return mapProfile(row);
|
return mapProfile(row);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import fastify, { type FastifyRequest } from 'fastify';
|
import fastify, { type FastifyRequest } from 'fastify';
|
||||||
import cors from '@fastify/cors';
|
import cors from '@fastify/cors';
|
||||||
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
||||||
import type { PrismaClient } from '@prisma/client';
|
|
||||||
import {
|
import {
|
||||||
createPostgresConnector,
|
createGatewayPostgresConnector,
|
||||||
createRedisConnector,
|
createRedisConnector,
|
||||||
|
type GatewayPrismaClient,
|
||||||
resolvePostgresConfigFromEnv,
|
resolvePostgresConfigFromEnv,
|
||||||
resolveRedisConfigFromEnv,
|
resolveRedisConfigFromEnv,
|
||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
@@ -22,13 +22,15 @@ import { RepositoryProfileStatusService } from './lobby/profileStatusService.js'
|
|||||||
|
|
||||||
export const createGatewayApiServer = async () => {
|
export const createGatewayApiServer = async () => {
|
||||||
const config = resolveGatewayApiConfigFromEnv();
|
const config = resolveGatewayApiConfigFromEnv();
|
||||||
const postgres = createPostgresConnector(resolvePostgresConfigFromEnv());
|
const postgres = createGatewayPostgresConnector(
|
||||||
|
resolvePostgresConfigFromEnv({ schema: config.dbSchema })
|
||||||
|
);
|
||||||
const redis = createRedisConnector(resolveRedisConfigFromEnv());
|
const redis = createRedisConnector(resolveRedisConfigFromEnv());
|
||||||
await postgres.connect();
|
await postgres.connect();
|
||||||
await redis.connect();
|
await redis.connect();
|
||||||
|
|
||||||
const users = createPostgresUserRepository(
|
const users = createPostgresUserRepository(
|
||||||
postgres.prisma as PrismaClient
|
postgres.prisma as GatewayPrismaClient
|
||||||
);
|
);
|
||||||
const sessions = new RedisGatewaySessionService(redis.client, {
|
const sessions = new RedisGatewaySessionService(redis.client, {
|
||||||
keyPrefix: config.redisKeyPrefix,
|
keyPrefix: config.redisKeyPrefix,
|
||||||
@@ -48,7 +50,7 @@ export const createGatewayApiServer = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { orchestrator, profiles } = createGatewayOrchestrator(
|
const { orchestrator, profiles } = createGatewayOrchestrator(
|
||||||
postgres.prisma as PrismaClient,
|
postgres.prisma as GatewayPrismaClient,
|
||||||
config,
|
config,
|
||||||
process.env
|
process.env
|
||||||
);
|
);
|
||||||
@@ -82,7 +84,7 @@ export const createGatewayApiServer = async () => {
|
|||||||
profileStatus,
|
profileStatus,
|
||||||
adminToken: config.adminToken,
|
adminToken: config.adminToken,
|
||||||
requestHeaders: req.headers,
|
requestHeaders: req.headers,
|
||||||
prisma: postgres.prisma as PrismaClient,
|
prisma: postgres.prisma as GatewayPrismaClient,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ Build outputs should be emitted to `/dist/{profileName}` per profile to keep
|
|||||||
deployments predictable. Profiles are server+scenario pairs, and scenario
|
deployments predictable. Profiles are server+scenario pairs, and scenario
|
||||||
selection is required because it drives unit sets and DB settings.
|
selection is required because it drives unit sets and DB settings.
|
||||||
|
|
||||||
|
## Database Schemas (Gateway vs Game)
|
||||||
|
|
||||||
|
Gateway uses a shared schema (default `public`) for login/profile state, while
|
||||||
|
each game profile runs against its own schema. This keeps gateway data stable
|
||||||
|
and allows profile-scoped game data resets.
|
||||||
|
|
||||||
|
- Gateway schema: `GATEWAY_DB_SCHEMA` (default `public`)
|
||||||
|
- Game schema: `PROFILE` value (e.g., `hwe`, `che`)
|
||||||
|
- Optional override for gateway DB URL: `GATEWAY_DATABASE_URL`
|
||||||
|
|
||||||
## Suggested Build Pattern
|
## Suggested Build Pattern
|
||||||
|
|
||||||
- Wrapper script under `tools/build-scripts`
|
- Wrapper script under `tools/build-scripts`
|
||||||
|
|||||||
@@ -10,11 +10,16 @@
|
|||||||
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/infra --watch",
|
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/infra --watch",
|
||||||
"lint": "node -e \"console.log('lint not configured')\"",
|
"lint": "node -e \"console.log('lint not configured')\"",
|
||||||
"test": "node -e \"console.log('test not configured')\"",
|
"test": "node -e \"console.log('test not configured')\"",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc -b",
|
||||||
|
"prisma:generate:game": "prisma generate --schema prisma/game.prisma",
|
||||||
|
"prisma:generate:gateway": "prisma generate --schema prisma/gateway.prisma",
|
||||||
|
"prisma:db:push:game": "prisma db push --schema prisma/game.prisma",
|
||||||
|
"prisma:db:push:gateway": "prisma db push --schema prisma/gateway.prisma"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@prisma/client-runtime-utils": "^7.2.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"redis": "^5.10.0"
|
"redis": "^5.10.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,20 +2,33 @@ import 'dotenv/config';
|
|||||||
|
|
||||||
import { defineConfig } from 'prisma/config';
|
import { defineConfig } from 'prisma/config';
|
||||||
|
|
||||||
|
const resolveSchemaName = (value: string | undefined): string => {
|
||||||
|
if (!value) {
|
||||||
|
return 'public';
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : 'public';
|
||||||
|
};
|
||||||
|
|
||||||
const buildDatabaseUrlFromEnv = (): string => {
|
const buildDatabaseUrlFromEnv = (): string => {
|
||||||
const host = process.env.POSTGRES_HOST ?? '127.0.0.1';
|
const host = process.env.POSTGRES_HOST ?? '127.0.0.1';
|
||||||
const port = process.env.POSTGRES_PORT ?? '15432';
|
const port = process.env.POSTGRES_PORT ?? '15432';
|
||||||
const user = process.env.POSTGRES_USER ?? 'sammo';
|
const user = process.env.POSTGRES_USER ?? 'sammo';
|
||||||
const password = process.env.POSTGRES_PASSWORD ?? '';
|
const password = process.env.POSTGRES_PASSWORD ?? '';
|
||||||
const dbName = process.env.POSTGRES_DB ?? 'sammo';
|
const dbName = process.env.POSTGRES_DB ?? 'sammo';
|
||||||
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
|
const schema = resolveSchemaName(
|
||||||
|
process.env.POSTGRES_SCHEMA ?? process.env.DATABASE_SCHEMA
|
||||||
|
);
|
||||||
|
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=${schema}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const databaseUrl =
|
const databaseUrl =
|
||||||
process.env.DATABASE_URL ?? buildDatabaseUrlFromEnv();
|
process.env.DATABASE_URL ?? buildDatabaseUrlFromEnv();
|
||||||
|
|
||||||
|
const schemaPath = process.env.PRISMA_SCHEMA ?? 'prisma/game.prisma';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: 'prisma/schema.prisma',
|
schema: schemaPath,
|
||||||
datasource: {
|
datasource: {
|
||||||
url: databaseUrl,
|
url: databaseUrl,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
output = "./generated/game"
|
||||||
|
engineType = "binary"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@@ -22,74 +24,6 @@ enum LogCategory {
|
|||||||
USER
|
USER
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OAuthType {
|
|
||||||
NONE
|
|
||||||
KAKAO
|
|
||||||
}
|
|
||||||
|
|
||||||
enum GatewayProfileStatus {
|
|
||||||
RESERVED
|
|
||||||
PREOPEN
|
|
||||||
RUNNING
|
|
||||||
PAUSED
|
|
||||||
COMPLETED
|
|
||||||
STOPPED
|
|
||||||
DISABLED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum GatewayBuildStatus {
|
|
||||||
IDLE
|
|
||||||
QUEUED
|
|
||||||
RUNNING
|
|
||||||
FAILED
|
|
||||||
SUCCEEDED
|
|
||||||
}
|
|
||||||
|
|
||||||
model AppUser {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
loginId String @unique @map("login_id")
|
|
||||||
displayName String @map("display_name")
|
|
||||||
passwordHash String @map("password_hash")
|
|
||||||
passwordSalt String @map("password_salt")
|
|
||||||
roles Json @default(dbgenerated("'[]'::jsonb"))
|
|
||||||
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
|
||||||
oauthType OAuthType @default(NONE) @map("oauth_type")
|
|
||||||
oauthId String? @unique @map("oauth_id")
|
|
||||||
email String? @unique
|
|
||||||
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
|
||||||
lastLoginAt DateTime? @map("last_login_at")
|
|
||||||
|
|
||||||
@@map("app_user")
|
|
||||||
}
|
|
||||||
|
|
||||||
model GatewayProfile {
|
|
||||||
profileName String @id @map("profile_name")
|
|
||||||
profile String
|
|
||||||
scenario String
|
|
||||||
apiPort Int @map("api_port")
|
|
||||||
status GatewayProfileStatus
|
|
||||||
buildStatus GatewayBuildStatus @default(IDLE) @map("build_status")
|
|
||||||
buildCommitSha String? @map("build_commit_sha")
|
|
||||||
buildWorkspace String? @map("build_workspace")
|
|
||||||
buildLastUsedAt DateTime? @map("build_last_used_at")
|
|
||||||
preopenAt DateTime? @map("preopen_at")
|
|
||||||
openAt DateTime? @map("open_at")
|
|
||||||
scheduledStartAt DateTime? @map("scheduled_start_at")
|
|
||||||
buildRequestedAt DateTime? @map("build_requested_at")
|
|
||||||
buildStartedAt DateTime? @map("build_started_at")
|
|
||||||
buildCompletedAt DateTime? @map("build_completed_at")
|
|
||||||
buildError String? @map("build_error")
|
|
||||||
lastError String? @map("last_error")
|
|
||||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
|
||||||
|
|
||||||
@@unique([profile, scenario])
|
|
||||||
@@map("gateway_profile")
|
|
||||||
}
|
|
||||||
|
|
||||||
model WorldState {
|
model WorldState {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
scenarioCode String @map("scenario_code")
|
scenarioCode String @map("scenario_code")
|
||||||
@@ -272,10 +206,3 @@ model LogEntry {
|
|||||||
@@index([userId, category, id])
|
@@index([userId, category, id])
|
||||||
@@map("log_entry")
|
@@map("log_entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
model SystemSetting {
|
|
||||||
id Int @id @default(1) @map("no")
|
|
||||||
notice String @default("") @map("notice")
|
|
||||||
|
|
||||||
@@map("system")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
output = "./generated/gateway"
|
||||||
|
engineType = "binary"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OAuthType {
|
||||||
|
NONE
|
||||||
|
KAKAO
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GatewayProfileStatus {
|
||||||
|
RESERVED
|
||||||
|
PREOPEN
|
||||||
|
RUNNING
|
||||||
|
PAUSED
|
||||||
|
COMPLETED
|
||||||
|
STOPPED
|
||||||
|
DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GatewayBuildStatus {
|
||||||
|
IDLE
|
||||||
|
QUEUED
|
||||||
|
RUNNING
|
||||||
|
FAILED
|
||||||
|
SUCCEEDED
|
||||||
|
}
|
||||||
|
|
||||||
|
model AppUser {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
loginId String @unique @map("login_id")
|
||||||
|
displayName String @map("display_name")
|
||||||
|
passwordHash String @map("password_hash")
|
||||||
|
passwordSalt String @map("password_salt")
|
||||||
|
roles Json @default(dbgenerated("'[]'::jsonb"))
|
||||||
|
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
||||||
|
oauthType OAuthType @default(NONE) @map("oauth_type")
|
||||||
|
oauthId String? @unique @map("oauth_id")
|
||||||
|
email String? @unique
|
||||||
|
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
lastLoginAt DateTime? @map("last_login_at")
|
||||||
|
|
||||||
|
@@map("app_user")
|
||||||
|
}
|
||||||
|
|
||||||
|
model GatewayProfile {
|
||||||
|
profileName String @id @map("profile_name")
|
||||||
|
profile String
|
||||||
|
scenario String
|
||||||
|
apiPort Int @map("api_port")
|
||||||
|
status GatewayProfileStatus
|
||||||
|
buildStatus GatewayBuildStatus @default(IDLE) @map("build_status")
|
||||||
|
buildCommitSha String? @map("build_commit_sha")
|
||||||
|
buildWorkspace String? @map("build_workspace")
|
||||||
|
buildLastUsedAt DateTime? @map("build_last_used_at")
|
||||||
|
preopenAt DateTime? @map("preopen_at")
|
||||||
|
openAt DateTime? @map("open_at")
|
||||||
|
scheduledStartAt DateTime? @map("scheduled_start_at")
|
||||||
|
buildRequestedAt DateTime? @map("build_requested_at")
|
||||||
|
buildStartedAt DateTime? @map("build_started_at")
|
||||||
|
buildCompletedAt DateTime? @map("build_completed_at")
|
||||||
|
buildError String? @map("build_error")
|
||||||
|
lastError String? @map("last_error")
|
||||||
|
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@unique([profile, scenario])
|
||||||
|
@@map("gateway_profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SystemSetting {
|
||||||
|
id Int @id @default(1) @map("no")
|
||||||
|
notice String @default("") @map("notice")
|
||||||
|
|
||||||
|
@@map("system")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -1,4 +1,19 @@
|
|||||||
export * from './postgres.js';
|
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 './db.js';
|
||||||
export * from './logRepository.js';
|
export * from './logRepository.js';
|
||||||
export * from './redis.js';
|
export * from './redis.js';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { LogCategory, LogScope } from '@prisma/client';
|
import { LogCategory, LogScope, type GamePrisma, type GamePrismaClient } from './gamePrisma.js';
|
||||||
import type { Prisma, PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface LogQueryOptions {
|
export interface LogQueryOptions {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -21,9 +20,9 @@ export interface LogEntryView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildPaginationWhere = (
|
const buildPaginationWhere = (
|
||||||
base: Prisma.LogEntryWhereInput,
|
base: GamePrisma.LogEntryWhereInput,
|
||||||
options: LogQueryOptions
|
options: LogQueryOptions
|
||||||
): Prisma.LogEntryWhereInput => {
|
): GamePrisma.LogEntryWhereInput => {
|
||||||
if (options.beforeId) {
|
if (options.beforeId) {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -34,16 +33,16 @@ const buildPaginationWhere = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildFindArgs = (
|
const buildFindArgs = (
|
||||||
where: Prisma.LogEntryWhereInput,
|
where: GamePrisma.LogEntryWhereInput,
|
||||||
options: LogQueryOptions
|
options: LogQueryOptions
|
||||||
): Prisma.LogEntryFindManyArgs => ({
|
): GamePrisma.LogEntryFindManyArgs => ({
|
||||||
where: buildPaginationWhere(where, options),
|
where: buildPaginationWhere(where, options),
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take: options.limit ?? 50,
|
take: options.limit ?? 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class LogRepository {
|
export class LogRepository {
|
||||||
constructor(private readonly prisma: PrismaClient) {}
|
constructor(private readonly prisma: GamePrismaClient) {}
|
||||||
|
|
||||||
// 전역(시스템) 로그 조회
|
// 전역(시스템) 로그 조회
|
||||||
async listSystemLogs(
|
async listSystemLogs(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { PrismaPg } from '@prisma/adapter-pg';
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
@@ -22,21 +21,75 @@ export interface PostgresConnector<TClient = unknown> {
|
|||||||
disconnect(): Promise<void>;
|
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 = (
|
const buildDatabaseUrlFromEnv = (
|
||||||
env: NodeJS.ProcessEnv
|
env: NodeJS.ProcessEnv,
|
||||||
|
schemaOverride?: string
|
||||||
): string => {
|
): string => {
|
||||||
const host = env.POSTGRES_HOST ?? '127.0.0.1';
|
const host = env.POSTGRES_HOST ?? '127.0.0.1';
|
||||||
const port = env.POSTGRES_PORT ?? '15432';
|
const port = env.POSTGRES_PORT ?? '15432';
|
||||||
const user = env.POSTGRES_USER ?? 'sammo';
|
const user = env.POSTGRES_USER ?? 'sammo';
|
||||||
const password = env.POSTGRES_PASSWORD ?? '';
|
const password = env.POSTGRES_PASSWORD ?? '';
|
||||||
const dbName = env.POSTGRES_DB ?? 'sammo';
|
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 = (
|
export const resolvePostgresConfigFromEnv = (
|
||||||
env: NodeJS.ProcessEnv = process.env
|
options: { env?: NodeJS.ProcessEnv; schema?: string } = {}
|
||||||
): PostgresConfig => {
|
): 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) {
|
if (!url) {
|
||||||
throw new Error('DATABASE_URL is required to create a Postgres client.');
|
throw new Error('DATABASE_URL is required to create a Postgres client.');
|
||||||
}
|
}
|
||||||
@@ -44,23 +97,32 @@ export const resolvePostgresConfigFromEnv = (
|
|||||||
return { url };
|
return { url };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createPostgresConnector = (
|
export const createPostgresConnector = <TClient>(
|
||||||
config: PostgresConfig
|
config: PostgresConfig,
|
||||||
): PostgresConnector => {
|
createClient: PrismaClientFactory<TClient>
|
||||||
|
): PostgresConnector<TClient> => {
|
||||||
|
const schema =
|
||||||
|
extractSchemaFromDatabaseUrl(config.url) ??
|
||||||
|
process.env.POSTGRES_SCHEMA ??
|
||||||
|
process.env.DATABASE_SCHEMA;
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
connectionString: config.url,
|
connectionString: config.url,
|
||||||
|
...(schema ? { options: `-c search_path=${schema}` } : {}),
|
||||||
});
|
});
|
||||||
const adapter = new PrismaPg(pool);
|
const adapter = new PrismaPg(
|
||||||
const prisma = new PrismaClient({
|
pool,
|
||||||
|
schema ? { schema } : undefined
|
||||||
|
);
|
||||||
|
const prisma = createClient({
|
||||||
adapter,
|
adapter,
|
||||||
log: config.log,
|
log: config.log,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prisma,
|
prisma,
|
||||||
connect: () => prisma.$connect(),
|
connect: () => (prisma as { $connect: () => Promise<void> }).$connect(),
|
||||||
disconnect: async () => {
|
disconnect: async () => {
|
||||||
await prisma.$disconnect();
|
await (prisma as { $disconnect: () => Promise<void> }).$disconnect();
|
||||||
await pool.end();
|
await pool.end();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+3
@@ -201,6 +201,9 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^7.2.0
|
specifier: ^7.2.0
|
||||||
version: 7.2.0(prisma@7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)
|
version: 7.2.0(prisma@7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)
|
||||||
|
'@prisma/client-runtime-utils':
|
||||||
|
specifier: ^7.2.0
|
||||||
|
version: 7.2.0
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.3
|
specifier: ^8.16.3
|
||||||
version: 8.16.3
|
version: 8.16.3
|
||||||
|
|||||||
Reference in New Issue
Block a user