mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Add support for individual database environment variables (#9344)
* Add support for individual database environment variables - Add DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD env vars - Implement mutual exclusivity validation between DATABASE_URL and individual components - Add effectiveDatabaseUrl getter to construct URL from individual components - Update database connection logic to use new configuration options - Ensure backward compatibility with existing DATABASE_URL configuration Resolves: https://github.com/outline/outline/discussions/9158 * Refactor database configuration methods - Move effectiveDatabaseUrl method from env.ts to database.ts as getEffectiveDatabaseUrl function - Remove validateDatabaseConfiguration method from env.ts as validation is handled by decorators - Maintain clean separation of concerns between environment and database modules * Pass database options directly to Sequelize constructor - Replace URL construction with direct Sequelize configuration object - Support both DATABASE_URL string and individual component object configurations - Maintain common Sequelize options for both configuration types - Improve error messaging for different configuration scenarios * remove spurious comments * tsc --------- Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
+50
-1
@@ -16,7 +16,11 @@ import {
|
||||
import uniq from "lodash/uniq";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { Day, Hour } from "@shared/utils/time";
|
||||
import { CannotUseWith, CannotUseWithout } from "@server/utils/validators";
|
||||
import {
|
||||
CannotUseWith,
|
||||
CannotUseWithout,
|
||||
CannotUseWithAny,
|
||||
} from "@server/utils/validators";
|
||||
import Deprecated from "./models/decorators/Deprecated";
|
||||
import { getArg } from "./utils/args";
|
||||
import { Public, PublicEnvironmentRegister } from "./utils/decorators/Public";
|
||||
@@ -79,8 +83,53 @@ export class Environment {
|
||||
allow_underscores: true,
|
||||
protocols: ["postgres", "postgresql"],
|
||||
})
|
||||
@CannotUseWithAny([
|
||||
"DATABASE_HOST",
|
||||
"DATABASE_PORT",
|
||||
"DATABASE_NAME",
|
||||
"DATABASE_USER",
|
||||
"DATABASE_PASSWORD",
|
||||
])
|
||||
public DATABASE_URL = environment.DATABASE_URL ?? "";
|
||||
|
||||
/**
|
||||
* Database host for individual component configuration.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWith("DATABASE_URL")
|
||||
public DATABASE_HOST = this.toOptionalString(environment.DATABASE_HOST);
|
||||
|
||||
/**
|
||||
* Database port for individual component configuration.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@CannotUseWith("DATABASE_URL")
|
||||
public DATABASE_PORT = this.toOptionalNumber(environment.DATABASE_PORT);
|
||||
|
||||
/**
|
||||
* Database name for individual component configuration.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWith("DATABASE_URL")
|
||||
public DATABASE_NAME = this.toOptionalString(environment.DATABASE_NAME);
|
||||
|
||||
/**
|
||||
* Database user for individual component configuration.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWith("DATABASE_URL")
|
||||
public DATABASE_USER = this.toOptionalString(environment.DATABASE_USER);
|
||||
|
||||
/**
|
||||
* Database password for individual component configuration.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWith("DATABASE_URL")
|
||||
public DATABASE_PASSWORD = this.toOptionalString(
|
||||
environment.DATABASE_PASSWORD
|
||||
);
|
||||
|
||||
/**
|
||||
* An optional database schema.
|
||||
*/
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
import path from "path";
|
||||
import { InferAttributes, InferCreationAttributes } from "sequelize";
|
||||
import sequelizeStrictAttributes from "sequelize-strict-attributes";
|
||||
import { Sequelize } from "sequelize-typescript";
|
||||
import { Sequelize, SequelizeOptions } from "sequelize-typescript";
|
||||
import { Umzug, SequelizeStorage, MigrationError } from "umzug";
|
||||
import env from "@server/env";
|
||||
import Model from "@server/models/base/Model";
|
||||
import Logger from "../logging/Logger";
|
||||
import * as models from "../models";
|
||||
|
||||
/**
|
||||
* Returns database configuration for Sequelize constructor.
|
||||
* Either uses DATABASE_URL or constructs options from individual components.
|
||||
*/
|
||||
function getDatabaseConfig() {
|
||||
if (env.DATABASE_URL) {
|
||||
return env.DATABASE_URL;
|
||||
}
|
||||
|
||||
// If using individual components, return Sequelize options object
|
||||
if (env.DATABASE_HOST && env.DATABASE_NAME && env.DATABASE_USER) {
|
||||
return {
|
||||
database: env.DATABASE_NAME,
|
||||
username: env.DATABASE_USER,
|
||||
password: env.DATABASE_PASSWORD || undefined,
|
||||
host: env.DATABASE_HOST,
|
||||
port: env.DATABASE_PORT || 5432,
|
||||
dialect: "postgres" as const,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"DATABASE_URL is not set or individual database components (DATABASE_HOST, DATABASE_NAME, DATABASE_USER) are not properly configured."
|
||||
);
|
||||
}
|
||||
|
||||
const isSSLDisabled = env.PGSSLMODE === "disable";
|
||||
const poolMax = env.DATABASE_CONNECTION_POOL_MAX ?? 5;
|
||||
const poolMin = env.DATABASE_CONNECTION_POOL_MIN ?? 0;
|
||||
const url = env.DATABASE_CONNECTION_POOL_URL || env.DATABASE_URL;
|
||||
const databaseConfig = env.DATABASE_CONNECTION_POOL_URL || getDatabaseConfig();
|
||||
const schema = env.DATABASE_SCHEMA;
|
||||
|
||||
export function createDatabaseInstance(
|
||||
databaseUrl: string,
|
||||
databaseConfig: string | object,
|
||||
input: {
|
||||
[key: string]: typeof Model<
|
||||
InferAttributes<Model>,
|
||||
@@ -24,7 +50,10 @@ export function createDatabaseInstance(
|
||||
}
|
||||
): Sequelize {
|
||||
try {
|
||||
const instance = new Sequelize(databaseUrl, {
|
||||
let instance;
|
||||
|
||||
// Common options for both URL and object configurations
|
||||
const commonOptions: SequelizeOptions = {
|
||||
logging: (msg) =>
|
||||
process.env.DEBUG?.includes("database") &&
|
||||
Logger.debug("database", msg),
|
||||
@@ -47,17 +76,27 @@ export function createDatabaseInstance(
|
||||
idle: 10000,
|
||||
},
|
||||
schema,
|
||||
});
|
||||
};
|
||||
|
||||
// If databaseConfig is a string, it's a URL; if it's an object, merge with common options
|
||||
if (typeof databaseConfig === "string") {
|
||||
instance = new Sequelize(databaseConfig, commonOptions);
|
||||
} else {
|
||||
instance = new Sequelize({ ...databaseConfig, ...commonOptions });
|
||||
}
|
||||
|
||||
sequelizeStrictAttributes(instance);
|
||||
return instance;
|
||||
} catch (error) {
|
||||
Logger.fatal(
|
||||
"Could not connect to database",
|
||||
databaseUrl
|
||||
typeof databaseConfig === "string"
|
||||
? new Error(
|
||||
`Failed to parse: "${databaseUrl}". Ensure special characters in database URL are encoded`
|
||||
`Failed to parse: "${databaseConfig}". Ensure special characters in database URL are encoded`
|
||||
)
|
||||
: new Error(
|
||||
`Failed to connect using database credentials. Please check DATABASE_HOST, DATABASE_NAME, DATABASE_USER configuration`
|
||||
)
|
||||
: new Error(`DATABASE_URL is not set.`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -131,7 +170,7 @@ export function createMigrationRunner(
|
||||
});
|
||||
}
|
||||
|
||||
export const sequelize = createDatabaseInstance(url, models);
|
||||
export const sequelize = createDatabaseInstance(databaseConfig, models);
|
||||
|
||||
export const migrations = createMigrationRunner(sequelize, [
|
||||
"migrations/*.js",
|
||||
|
||||
@@ -57,3 +57,33 @@ export function CannotUseWith(
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function CannotUseWithAny(
|
||||
properties: string[],
|
||||
validationOptions?: ValidationOptions
|
||||
) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "cannotUseWithAny",
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
constraints: properties,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate<T>(value: T, args: ValidationArguments) {
|
||||
if (value === undefined) {
|
||||
return true;
|
||||
}
|
||||
const obj = args.object as unknown as T;
|
||||
const forbiddenProperties = args.constraints as (keyof T)[];
|
||||
return forbiddenProperties.every((prop) => obj[prop] === undefined);
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${propertyName} cannot be used with any of: ${args.constraints.join(
|
||||
", "
|
||||
)}.`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user