feat: support separate db vars + file version

This commit is contained in:
diced
2025-10-16 21:02:17 -07:00
parent 69dfad201b
commit b4be96c7a8
5 changed files with 112 additions and 11 deletions

View File

@@ -1,9 +1,9 @@
import { log } from '@/lib/logger';
import { parse } from './transform';
import { readFileSync } from 'node:fs';
import { parse } from './transform';
export type EnvType = 'string' | 'string[]' | 'number' | 'boolean' | 'byte' | 'ms' | 'json';
export function env(property: string, env: string | string[], type: EnvType, isDb: boolean = false) {
export function env(property: string, env: string, type: EnvType, isDb: boolean = false) {
return {
variable: env,
property,
@@ -16,7 +16,14 @@ export const ENVS = [
env('core.port', 'CORE_PORT', 'number'),
env('core.hostname', 'CORE_HOSTNAME', 'string'),
env('core.secret', 'CORE_SECRET', 'string'),
env('core.databaseUrl', ['DATABASE_URL', 'CORE_DATABASE_URL'], 'string'),
env('core.databaseUrl', 'DATABASE_URL', 'string'),
// or
env('core.database.username', 'DATABASE_USERNAME', 'string', true),
env('core.database.password', 'DATABASE_PASSWORD', 'string', true),
env('core.database.host', 'DATABASE_HOST', 'string', true),
env('core.database.port', 'DATABASE_PORT', 'number', true),
env('core.database.name', 'DATABASE_NAME', 'string', true),
env('datasource.type', 'DATASOURCE_TYPE', 'string'),
env('datasource.s3.accessKeyId', 'DATASOURCE_S3_ACCESS_KEY_ID', 'string'),
@@ -161,11 +168,62 @@ export const PROP_TO_ENV: Record<string, string | string[]> = Object.fromEntries
ENVS.map((env) => [env.property, env.variable]),
);
export const REQUIRED_DB_VARS = [
'DATABASE_USERNAME',
'DATABASE_PASSWORD',
'DATABASE_HOST',
'DATABASE_PORT',
'DATABASE_NAME',
];
type EnvResult = {
env: Record<string, any>;
dbEnv: Record<string, any>;
};
export function checkDbVars(): boolean {
if (process.env.DATABASE_URL) return true;
for (let i = 0; i !== REQUIRED_DB_VARS.length; ++i) {
if (process.env[REQUIRED_DB_VARS[i]] === undefined) {
return false;
}
}
return true;
}
export function readDbVars(): Record<string, string> {
const logger = log('config').c('readDbVars');
if (process.env.DATABASE_URL) return { DATABASE_URL: process.env.DATABASE_URL };
const dbVars: Record<string, string> = {};
for (let i = 0; i !== REQUIRED_DB_VARS.length; ++i) {
const value = process.env[REQUIRED_DB_VARS[i]];
const valueFileName = process.env[`${REQUIRED_DB_VARS[i]}_FILE`];
if (valueFileName) {
try {
dbVars[REQUIRED_DB_VARS[i]] = readFileSync(valueFileName, 'utf-8').trim();
} catch {
logger.error(`Failed to read database env value from file for ${REQUIRED_DB_VARS[i]}. Exiting...`);
process.exit(1);
}
} else if (value) {
dbVars[REQUIRED_DB_VARS[i]] = value;
}
}
if (!Object.keys(dbVars).length || Object.keys(dbVars).length !== REQUIRED_DB_VARS.length) {
logger.error(
`No database environment variables found (DATABASE_URL or all of [${REQUIRED_DB_VARS.join(', ')}]), exiting...`,
);
process.exit(1);
}
return dbVars;
}
export function readEnv(): EnvResult {
const logger = log('config').c('readEnv');
const envResult: EnvResult = {
@@ -175,9 +233,6 @@ export function readEnv(): EnvResult {
for (let i = 0; i !== ENVS.length; ++i) {
const env = ENVS[i];
if (Array.isArray(env.variable)) {
env.variable = env.variable.find((v) => process.env[v] !== undefined) || 'DATABASE_URL';
}
let value = process.env[env.variable];
const valueFileName = process.env[`${env.variable}_FILE`];

View File

@@ -14,6 +14,13 @@ export const rawConfig: any = {
returnHttpsUrls: undefined,
tempDirectory: undefined,
trustProxy: undefined,
database: {
username: undefined,
password: undefined,
host: undefined,
port: undefined,
name: undefined,
},
},
chunks: {
max: undefined,

View File

@@ -67,7 +67,6 @@ export const schema = z.object({
});
}
}),
databaseUrl: z.url(),
returnHttpsUrls: z.boolean().default(false),
defaultDomain: z.string().nullable().default(null),
tempDirectory: z
@@ -75,6 +74,29 @@ export const schema = z.object({
.transform((s) => resolve(s))
.default(join(tmpdir(), 'zipline')),
trustProxy: z.boolean().default(false),
databaseUrl: z.url(),
database: z
.object({
username: z.string().nullable().default(null),
password: z.string().nullable().default(null),
host: z.string().nullable().default(null),
port: z.number().nullable().default(null),
name: z.string().nullable().default(null),
})
.superRefine((val, c) => {
const values = Object.values(val);
const someSet = values.some((v) => v !== null);
const allSet = values.every((v) => v !== null);
if (someSet && !allSet) {
c.addIssue({
code: 'custom',
message: 'If one database field is set, all fields must be set',
});
}
}),
}),
chunks: z.object({
max: z.string().default('95mb'),

View File

@@ -4,6 +4,7 @@ import { type Prisma, PrismaClient } from '@/prisma/client';
import { metadataSchema } from './models/incompleteFile';
import { metricDataSchema } from './models/metric';
import { userViewSchema } from './models/user';
import { readDbVars, REQUIRED_DB_VARS } from '../config/read/env';
const building = !!process.env.ZIPLINE_BUILD;
@@ -31,12 +32,27 @@ function parseDbLog(env: string): Prisma.LogLevel[] {
.filter((v) => v) as unknown as Prisma.LogLevel[];
}
function pgConnectionString() {
const vars = readDbVars();
if (vars.DATABASE_URL) return vars.DATABASE_URL;
return `postgresql://${vars.DATABASE_USERNAME}:${vars.DATABASE_PASSWORD}@${vars.DATABASE_HOST}:${vars.DATABASE_PORT}/${vars.DATABASE_NAME}`;
}
function getClient() {
const logger = log('db');
logger.info('connecting to database ' + process.env.DATABASE_URL);
const connectionString = pgConnectionString();
if (!connectionString) {
logger.error(`either DATABASE_URL or all of [${REQUIRED_DB_VARS.join(', ')}] not set, exiting...`);
process.exit(1);
}
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
process.env.DATABASE_URL = connectionString;
logger.info('connecting to database', { url: connectionString });
const adapter = new PrismaPg({ connectionString });
const client = new PrismaClient({
adapter,
log: process.env.ZIPLINE_DB_LOG ? parseDbLog(process.env.ZIPLINE_DB_LOG) : undefined,

View File

@@ -1,5 +1,6 @@
import { bytes } from '@/lib/bytes';
import { reloadSettings } from '@/lib/config';
import { checkDbVars, REQUIRED_DB_VARS } from '@/lib/config/read/env';
import { getDatasource } from '@/lib/datasource';
import { prisma } from '@/lib/db';
import { runMigrations } from '@/lib/db/migration';
@@ -46,8 +47,8 @@ async function main() {
const argv = process.argv.slice(2);
logger.info('starting zipline', { mode: MODE, version: version, argv });
if (!process.env.DATABASE_URL) {
logger.error('DATABASE_URL not set, exiting...');
if (!checkDbVars()) {
logger.error(`either DATABASE_URL or all of [${REQUIRED_DB_VARS.join(', ')}] not set, exiting...`);
process.exit(1);
}