fix(datasource): register entities and subscribers explicitly (#3375)

This commit is contained in:
fallenbagel
2026-08-31 05:23:12 +05:00
committed by GitHub
parent 2759058aeb
commit 0f79ee663c
6 changed files with 103 additions and 11 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"preinstall": "npx only-allow pnpm",
"postinstall": "next telemetry disable",
"dev": "nodemon -e ts,json,yml --watch server --watch seerr-api.yml --exec 'ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts'",
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.json",
"build:server": "tsc --project server/tsconfig.build.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.build.json",
"build:next": "next build",
"build": "pnpm build:next && pnpm build:server",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache",
+52 -10
View File
@@ -1,3 +1,21 @@
import { Blocklist } from '@server/entity/Blocklist';
import DiscoverSlider from '@server/entity/DiscoverSlider';
import Issue from '@server/entity/Issue';
import IssueComment from '@server/entity/IssueComment';
import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
import OverrideRule from '@server/entity/OverrideRule';
import Season from '@server/entity/Season';
import SeasonRequest from '@server/entity/SeasonRequest';
import { Session } from '@server/entity/Session';
import { User } from '@server/entity/User';
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
import { UserSettings } from '@server/entity/UserSettings';
import { Watchlist } from '@server/entity/Watchlist';
import { IssueCommentSubscriber } from '@server/subscriber/IssueCommentSubscriber';
import { IssueSubscriber } from '@server/subscriber/IssueSubscriber';
import { MediaRequestSubscriber } from '@server/subscriber/MediaRequestSubscriber';
import { MediaSubscriber } from '@server/subscriber/MediaSubscriber';
import fs from 'fs';
import type { TlsOptions } from 'tls';
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
@@ -5,6 +23,30 @@ import { DataSource } from 'typeorm';
const DB_SSL_PREFIX = 'DB_SSL_';
const entities = [
Blocklist,
DiscoverSlider,
Issue,
IssueComment,
Media,
MediaRequest,
OverrideRule,
Season,
SeasonRequest,
Session,
User,
UserPushSubscription,
UserSettings,
Watchlist,
];
const subscribers = [
IssueCommentSubscriber,
IssueSubscriber,
MediaRequestSubscriber,
MediaSubscriber,
];
function boolFromEnv(envVar: string, defaultVal = false) {
if (process.env[envVar]) {
return process.env[envVar]?.toLowerCase() === 'true';
@@ -53,9 +95,9 @@ const testConfig: DataSourceOptions = {
synchronize: true,
dropSchema: true,
logging: boolFromEnv('DB_LOG_QUERIES'),
entities: ['server/entity/**/*.ts'],
entities,
migrations: ['server/migration/sqlite/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
subscribers,
};
const devConfig: DataSourceOptions = {
@@ -67,9 +109,9 @@ const devConfig: DataSourceOptions = {
migrationsRun: false,
logging: boolFromEnv('DB_LOG_QUERIES'),
enableWAL: true,
entities: ['server/entity/**/*.ts'],
entities,
migrations: ['server/migration/sqlite/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
subscribers,
};
const prodConfig: DataSourceOptions = {
@@ -81,9 +123,9 @@ const prodConfig: DataSourceOptions = {
migrationsRun: false,
logging: boolFromEnv('DB_LOG_QUERIES'),
enableWAL: true,
entities: ['dist/entity/**/*.js'],
entities,
migrations: ['dist/migration/sqlite/**/*.js'],
subscribers: ['dist/subscriber/**/*.js'],
subscribers,
};
const postgresDevConfig: DataSourceOptions = {
@@ -102,9 +144,9 @@ const postgresDevConfig: DataSourceOptions = {
synchronize: false,
migrationsRun: true,
logging: boolFromEnv('DB_LOG_QUERIES'),
entities: ['server/entity/**/*.ts'],
entities,
migrations: ['server/migration/postgres/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
subscribers,
};
const postgresProdConfig: DataSourceOptions = {
@@ -123,9 +165,9 @@ const postgresProdConfig: DataSourceOptions = {
synchronize: false,
migrationsRun: false,
logging: boolFromEnv('DB_LOG_QUERIES'),
entities: ['dist/entity/**/*.js'],
entities,
migrations: ['dist/migration/postgres/**/*.js'],
subscribers: ['dist/subscriber/**/*.js'],
subscribers,
};
export const isPgsql = process.env.DB_TYPE === 'postgres';
+1
View File
@@ -12,6 +12,7 @@ const prepareDb = async () => {
await seedTestDb({
preserveDb: process.env.PRESERVE_DB === 'true',
withMigrations: process.env.WITH_MIGRATIONS === 'true',
allowOutsideTest: true,
});
};
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["**/*.test.ts"]
}
+29
View File
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
import { resetTestDb, seedTestDb } from '@server/utils/seedTestDb';
// NODE_ENV is typed readonly, so it is swapped through the whole env object
function setNodeEnv(value: string | undefined) {
Object.assign(process.env, { NODE_ENV: value });
}
describe('test database guard', () => {
const originalNodeEnv = process.env.NODE_ENV;
afterEach(() => {
setNodeEnv(originalNodeEnv);
});
it('refuses to seed when NODE_ENV is not test', async () => {
setNodeEnv('production');
await assert.rejects(() => seedTestDb(), /Refusing to seed/);
});
it('refuses to reset when NODE_ENV is not test', async () => {
setNodeEnv('production');
await assert.rejects(() => resetTestDb(), /Refusing to reset/);
});
});
+16
View File
@@ -8,6 +8,8 @@ export interface SeedDbOptions {
preserveDb?: boolean;
/** If true, runs migrations instead of synchronizing schema */
withMigrations?: boolean;
/** If true, permits seeding while NODE_ENV is not test */
allowOutsideTest?: boolean;
}
// Precomputed bcrypt hash of 'test1234'. We precompute this to avoid
@@ -15,6 +17,16 @@ export interface SeedDbOptions {
const TEST_USER_PASSWORD_HASH =
'$2b$12$Z5V2P5HZgmx4/AnWFMZN1.aD5AM1NucNi.mhNTSQ9oVtmdzu7Le/a';
function assertTestDatabase(operation: string, allowOutsideTest = false): void {
if (allowOutsideTest || process.env.NODE_ENV === 'test') {
return;
}
throw new Error(
`Refusing to ${operation} while NODE_ENV is not test: this drops every table and seeds accounts with a known password.`
);
}
/**
* Seeds test users into the database.
* Assumes the database schema is already set up.
@@ -68,6 +80,8 @@ async function seedTestUsers(): Promise<void> {
* Used by both Cypress tests and Vitest unit tests.
*/
export async function seedTestDb(options: SeedDbOptions = {}): Promise<void> {
assertTestDatabase('seed the test database', options.allowOutsideTest);
const dbConnection = dataSource.isInitialized
? dataSource
: await dataSource.initialize();
@@ -91,6 +105,8 @@ export async function seedTestDb(options: SeedDbOptions = {}): Promise<void> {
* Assumes DB has been initialized.
*/
export async function resetTestDb(): Promise<void> {
assertTestDatabase('reset the test database');
await dataSource.synchronize(true);
await seedTestUsers();
}