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:
@@ -10,11 +10,16 @@
|
||||
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/infra --watch",
|
||||
"lint": "node -e \"console.log('lint 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": {
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@prisma/client-runtime-utils": "^7.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"redis": "^5.10.0"
|
||||
},
|
||||
|
||||
@@ -2,20 +2,33 @@ import 'dotenv/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 host = process.env.POSTGRES_HOST ?? '127.0.0.1';
|
||||
const port = process.env.POSTGRES_PORT ?? '15432';
|
||||
const user = process.env.POSTGRES_USER ?? 'sammo';
|
||||
const password = process.env.POSTGRES_PASSWORD ?? '';
|
||||
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 =
|
||||
process.env.DATABASE_URL ?? buildDatabaseUrlFromEnv();
|
||||
|
||||
const schemaPath = process.env.PRISMA_SCHEMA ?? 'prisma/game.prisma';
|
||||
|
||||
export default defineConfig({
|
||||
schema: 'prisma/schema.prisma',
|
||||
schema: schemaPath,
|
||||
datasource: {
|
||||
url: databaseUrl,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "./generated/game"
|
||||
engineType = "binary"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@@ -22,74 +24,6 @@ enum LogCategory {
|
||||
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 {
|
||||
id Int @id @default(autoincrement())
|
||||
scenarioCode String @map("scenario_code")
|
||||
@@ -272,10 +206,3 @@ model LogEntry {
|
||||
@@index([userId, category, id])
|
||||
@@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 {
|
||||
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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user