174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
import { parse as parseSuperJson } from "superjson";
|
|
|
|
import { eq } from "drizzle-orm";
|
|
import type { SQLWrapper } from "drizzle-orm";
|
|
import { defaultServerSettings } from "@homarr/server-settings";
|
|
import { dbEnv } from "@homarr/core/infrastructure/db/env";
|
|
|
|
type ProxyRow = Record<string, unknown>;
|
|
type ProxySchema = {
|
|
onboarding: { step: SQLWrapper };
|
|
serverSettings: { settingKey: SQLWrapper; value: SQLWrapper };
|
|
};
|
|
type ProxyQuery = {
|
|
where(condition: unknown): ProxyQuery;
|
|
limit(count: number): Promise<ProxyRow[]>;
|
|
};
|
|
type ProxyDatabase = {
|
|
select(selection: Record<string, unknown>): { from(table: unknown): ProxyQuery };
|
|
};
|
|
|
|
type ProxyReader = {
|
|
closeAsync: () => Promise<void>;
|
|
getOnboardingStepAsync: () => Promise<string>;
|
|
getDefaultLocaleAsync: () => Promise<string>;
|
|
};
|
|
|
|
// The proxy is long-lived and only performs two single-row reads. Keep its
|
|
// independent pool deliberately smaller than the application pool.
|
|
const proxyPoolConnectionLimit = 1;
|
|
|
|
const createProxyReader = (database: unknown, schema: ProxySchema, closeAsync: () => Promise<void>): ProxyReader => {
|
|
const queryDatabase = database as ProxyDatabase;
|
|
|
|
return {
|
|
closeAsync,
|
|
async getOnboardingStepAsync() {
|
|
const rows = await queryDatabase.select({ step: schema.onboarding.step }).from(schema.onboarding).limit(1);
|
|
const step = rows[0]?.step;
|
|
return typeof step === "string" ? step : "start";
|
|
},
|
|
async getDefaultLocaleAsync() {
|
|
const rows = await queryDatabase
|
|
.select({ value: schema.serverSettings.value })
|
|
.from(schema.serverSettings)
|
|
.where(eq(schema.serverSettings.settingKey, "culture"))
|
|
.limit(1);
|
|
const value = rows[0]?.value;
|
|
if (typeof value !== "string") return defaultServerSettings.culture.defaultLocale;
|
|
|
|
try {
|
|
const culture: unknown = parseSuperJson(value);
|
|
if (
|
|
typeof culture === "object" &&
|
|
culture !== null &&
|
|
"defaultLocale" in culture &&
|
|
typeof culture.defaultLocale === "string"
|
|
) {
|
|
return culture.defaultLocale;
|
|
}
|
|
} catch {
|
|
// Fall through to the default when a legacy or malformed value cannot be decoded.
|
|
}
|
|
|
|
return defaultServerSettings.culture.defaultLocale;
|
|
},
|
|
};
|
|
};
|
|
|
|
const createProxyReaderAsync = async (): Promise<ProxyReader> => {
|
|
switch (dbEnv.DRIVER) {
|
|
case "better-sqlite3": {
|
|
const [{ default: Database }, { drizzle }, { proxySchema }] = await Promise.all([
|
|
import("better-sqlite3"),
|
|
import("drizzle-orm/better-sqlite3"),
|
|
import("./proxy/sqlite"),
|
|
]);
|
|
const connection = new Database(dbEnv.URL);
|
|
const database = drizzle(connection, { schema: proxySchema });
|
|
return createProxyReader(database, proxySchema, async () => {
|
|
connection.close();
|
|
});
|
|
}
|
|
case "mysql2": {
|
|
const [{ default: mysql }, { drizzle }, { proxySchema }] = await Promise.all([
|
|
import("mysql2"),
|
|
import("drizzle-orm/mysql2"),
|
|
import("./proxy/mysql"),
|
|
]);
|
|
const connection = dbEnv.HOST
|
|
? mysql.createPool({
|
|
host: dbEnv.HOST,
|
|
port: dbEnv.PORT,
|
|
database: dbEnv.NAME,
|
|
user: dbEnv.USER,
|
|
password: dbEnv.PASSWORD,
|
|
connectionLimit: proxyPoolConnectionLimit,
|
|
maxIdle: 1,
|
|
idleTimeout: 60_000,
|
|
enableKeepAlive: true,
|
|
})
|
|
: mysql.createPool({
|
|
uri: dbEnv.URL,
|
|
connectionLimit: proxyPoolConnectionLimit,
|
|
maxIdle: 1,
|
|
idleTimeout: 60_000,
|
|
enableKeepAlive: true,
|
|
});
|
|
const database = drizzle(connection, { schema: proxySchema, mode: "default" });
|
|
return createProxyReader(
|
|
database,
|
|
proxySchema,
|
|
async () =>
|
|
await new Promise<void>((resolve, reject) => {
|
|
connection.end((error) => {
|
|
if (error) reject(error);
|
|
else resolve();
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
case "node-postgres": {
|
|
const [{ Pool }, { drizzle }, { proxySchema }] = await Promise.all([
|
|
import("pg"),
|
|
import("drizzle-orm/node-postgres"),
|
|
import("./proxy/postgresql"),
|
|
]);
|
|
const connection = dbEnv.HOST
|
|
? new Pool({
|
|
host: dbEnv.HOST,
|
|
port: dbEnv.PORT,
|
|
database: dbEnv.NAME,
|
|
user: dbEnv.USER,
|
|
password: dbEnv.PASSWORD,
|
|
max: proxyPoolConnectionLimit,
|
|
idleTimeoutMillis: 60_000,
|
|
allowExitOnIdle: false,
|
|
})
|
|
: new Pool({
|
|
connectionString: dbEnv.URL,
|
|
max: proxyPoolConnectionLimit,
|
|
idleTimeoutMillis: 60_000,
|
|
allowExitOnIdle: false,
|
|
});
|
|
const database = drizzle({ client: connection, schema: proxySchema });
|
|
return createProxyReader(database, proxySchema, async () => {
|
|
await connection.end();
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
let proxyReaderPromise: Promise<ProxyReader> | null = null;
|
|
|
|
const getProxyReaderAsync = () => {
|
|
if (!proxyReaderPromise) {
|
|
proxyReaderPromise = createProxyReaderAsync().catch((error: unknown) => {
|
|
proxyReaderPromise = null;
|
|
throw error;
|
|
});
|
|
}
|
|
return proxyReaderPromise;
|
|
};
|
|
|
|
export const getOnboardingStepForProxyAsync = async () => (await getProxyReaderAsync()).getOnboardingStepAsync();
|
|
|
|
export const getDefaultLocaleForProxyAsync = async () => (await getProxyReaderAsync()).getDefaultLocaleAsync();
|
|
|
|
export const closeProxySettingsReaderAsync = async () => {
|
|
if (!proxyReaderPromise) return;
|
|
const reader = await proxyReaderPromise;
|
|
proxyReaderPromise = null;
|
|
await reader.closeAsync();
|
|
};
|