mirror of
https://github.com/immich-app/immich.git
synced 2026-03-04 09:57:33 +03:00
refactor: database connection parsing (#17852)
This commit is contained in:
@@ -80,21 +80,12 @@ describe('getEnv', () => {
|
||||
const { database } = getEnv();
|
||||
expect(database).toEqual({
|
||||
config: {
|
||||
kysely: expect.objectContaining({
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
database: 'immich',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
}),
|
||||
typeorm: expect.objectContaining({
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
database: 'immich',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
}),
|
||||
connectionType: 'parts',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
database: 'immich',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
},
|
||||
skipMigrations: false,
|
||||
vectorExtension: 'vectors',
|
||||
@@ -110,88 +101,9 @@ describe('getEnv', () => {
|
||||
it('should use DB_URL', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich';
|
||||
const { database } = getEnv();
|
||||
expect(database.config.kysely).toMatchObject({
|
||||
host: 'database1',
|
||||
password: 'postgres2',
|
||||
user: 'postgres1',
|
||||
port: 54_320,
|
||||
database: 'immich',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sslmode=require', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=prefer', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=verify-ca', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=verify-full', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: {} });
|
||||
});
|
||||
|
||||
it('should handle sslmode=no-verify', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: { rejectUnauthorized: false } });
|
||||
});
|
||||
|
||||
it('should handle ssl=true', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({ ssl: true });
|
||||
});
|
||||
|
||||
it('should reject invalid ssl', () => {
|
||||
process.env.DB_URL = 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid';
|
||||
|
||||
expect(() => getEnv()).toThrowError('Invalid ssl option: invalid');
|
||||
});
|
||||
|
||||
it('should handle socket: URLs', () => {
|
||||
process.env.DB_URL = 'socket:/run/postgresql?db=database1';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({
|
||||
host: '/run/postgresql',
|
||||
database: 'database1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sockets in postgres: URLs', () => {
|
||||
process.env.DB_URL = 'postgres:///database2?host=/path/to/socket';
|
||||
|
||||
const { database } = getEnv();
|
||||
|
||||
expect(database.config.kysely).toMatchObject({
|
||||
host: '/path/to/socket',
|
||||
database: 'database2',
|
||||
expect(database.config).toMatchObject({
|
||||
connectionType: 'url',
|
||||
url: 'postgres://postgres1:postgres2@database1:54320/immich',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,7 @@ import { Request, Response } from 'express';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { parse } from 'pg-connection-string';
|
||||
import { join } from 'node:path';
|
||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { EnvDto } from 'src/dtos/env.dto';
|
||||
@@ -22,9 +21,7 @@ import {
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
||||
import { isValidSsl, PostgresConnectionConfig } from 'src/utils/database';
|
||||
import { setDifference } from 'src/utils/set';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
export interface EnvData {
|
||||
host?: string;
|
||||
@@ -59,7 +56,7 @@ export interface EnvData {
|
||||
};
|
||||
|
||||
database: {
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: PostgresConnectionConfig };
|
||||
config: DatabaseConnectionParams;
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
@@ -152,14 +149,10 @@ const getEnv = (): EnvData => {
|
||||
const isProd = environment === ImmichEnvironment.PRODUCTION;
|
||||
const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
|
||||
const folders = {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
dist: resolve(`${__dirname}/..`),
|
||||
geodata: join(buildFolder, 'geodata'),
|
||||
web: join(buildFolder, 'www'),
|
||||
};
|
||||
|
||||
const databaseUrl = dto.DB_URL;
|
||||
|
||||
let redisConfig = {
|
||||
host: dto.REDIS_HOSTNAME || 'redis',
|
||||
port: dto.REDIS_PORT || 6379,
|
||||
@@ -191,30 +184,16 @@ const getEnv = (): EnvData => {
|
||||
}
|
||||
}
|
||||
|
||||
const parts = {
|
||||
connectionType: 'parts',
|
||||
host: dto.DB_HOSTNAME || 'database',
|
||||
port: dto.DB_PORT || 5432,
|
||||
username: dto.DB_USERNAME || 'postgres',
|
||||
password: dto.DB_PASSWORD || 'postgres',
|
||||
database: dto.DB_DATABASE_NAME || 'immich',
|
||||
} as const;
|
||||
|
||||
let parsedOptions: PostgresConnectionConfig = parts;
|
||||
if (dto.DB_URL) {
|
||||
const parsed = parse(dto.DB_URL);
|
||||
if (!isValidSsl(parsed.ssl)) {
|
||||
throw new Error(`Invalid ssl option: ${parsed.ssl}`);
|
||||
}
|
||||
|
||||
parsedOptions = {
|
||||
...parsed,
|
||||
ssl: parsed.ssl,
|
||||
host: parsed.host ?? undefined,
|
||||
port: parsed.port ? Number(parsed.port) : undefined,
|
||||
database: parsed.database ?? undefined,
|
||||
};
|
||||
}
|
||||
const databaseConnection: DatabaseConnectionParams = dto.DB_URL
|
||||
? { connectionType: 'url', url: dto.DB_URL }
|
||||
: {
|
||||
connectionType: 'parts',
|
||||
host: dto.DB_HOSTNAME || 'database',
|
||||
port: dto.DB_PORT || 5432,
|
||||
username: dto.DB_USERNAME || 'postgres',
|
||||
password: dto.DB_PASSWORD || 'postgres',
|
||||
database: dto.DB_DATABASE_NAME || 'immich',
|
||||
};
|
||||
|
||||
return {
|
||||
host: dto.IMMICH_HOST,
|
||||
@@ -269,21 +248,7 @@ const getEnv = (): EnvData => {
|
||||
},
|
||||
|
||||
database: {
|
||||
config: {
|
||||
typeorm: {
|
||||
type: 'postgres',
|
||||
entities: [],
|
||||
migrations: [`${folders.dist}/migrations` + '/*.{js,ts}'],
|
||||
subscribers: [],
|
||||
migrationsRun: false,
|
||||
synchronize: false,
|
||||
connectTimeoutMS: 10_000, // 10 seconds
|
||||
parseInt8: true,
|
||||
...(databaseUrl ? { connectionType: 'url', url: databaseUrl } : parts),
|
||||
},
|
||||
kysely: parsedOptions,
|
||||
},
|
||||
|
||||
config: databaseConnection,
|
||||
skipMigrations: dto.DB_SKIP_MIGRATIONS ?? false,
|
||||
vectorExtension: dto.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import AsyncLock from 'async-lock';
|
||||
import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { join, resolve } from 'node:path';
|
||||
import semver from 'semver';
|
||||
import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
||||
import { DB } from 'src/db';
|
||||
@@ -205,8 +205,29 @@ export class DatabaseRepository {
|
||||
const { rows } = await tableExists.execute(this.db);
|
||||
const hasTypeOrmMigrations = !!rows[0]?.result;
|
||||
if (hasTypeOrmMigrations) {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const dist = resolve(`${__dirname}/..`);
|
||||
|
||||
this.logger.debug('Running typeorm migrations');
|
||||
const dataSource = new DataSource(database.config.typeorm);
|
||||
const dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
entities: [],
|
||||
subscribers: [],
|
||||
migrations: [`${dist}/migrations` + '/*.{js,ts}'],
|
||||
migrationsRun: false,
|
||||
synchronize: false,
|
||||
connectTimeoutMS: 10_000, // 10 seconds
|
||||
parseInt8: true,
|
||||
...(database.config.connectionType === 'url'
|
||||
? { url: database.config.url }
|
||||
: {
|
||||
host: database.config.host,
|
||||
port: database.config.port,
|
||||
username: database.config.username,
|
||||
password: database.config.password,
|
||||
database: database.config.database,
|
||||
}),
|
||||
});
|
||||
await dataSource.initialize();
|
||||
await dataSource.runMigrations(options);
|
||||
await dataSource.destroy();
|
||||
|
||||
Reference in New Issue
Block a user