mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 01:59:06 +03:00
feat: schema-check (#25904)
This commit is contained in:
@@ -1,15 +1,59 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isAbsolute } from 'node:path';
|
||||
import { isAbsolute, join } from 'node:path';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { schemaDiff } from 'src/sql-tools';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
export type SchemaReport = {
|
||||
migrations: MigrationStatus[];
|
||||
drift: ReturnType<typeof schemaDiff>;
|
||||
};
|
||||
|
||||
type MigrationStatus = {
|
||||
name: string;
|
||||
status: 'applied' | 'missing' | 'deleted';
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CliService extends BaseService {
|
||||
async schemaReport(): Promise<SchemaReport> {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const allFiles = await this.storageRepository.readdir(join(__dirname, '../schema/migrations'));
|
||||
const files = allFiles.filter((file) => file.endsWith('.js')).map((file) => file.slice(0, -3));
|
||||
const rows = await this.databaseRepository.getMigrations();
|
||||
const filesSet = new Set(files);
|
||||
const rowsSet = new Set(rows.map((item) => item.name));
|
||||
const combined = [...filesSet, ...rowsSet].toSorted();
|
||||
|
||||
const migrations: MigrationStatus[] = [];
|
||||
|
||||
for (const name of combined) {
|
||||
if (filesSet.has(name) && rowsSet.has(name)) {
|
||||
migrations.push({ name, status: 'applied' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filesSet.has(name) && !rowsSet.has(name)) {
|
||||
migrations.push({ name, status: 'missing' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!filesSet.has(name) && rowsSet.has(name)) {
|
||||
migrations.push({ name, status: 'deleted' });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const drift = await this.databaseRepository.getSchemaDrift();
|
||||
|
||||
return { migrations, drift };
|
||||
}
|
||||
|
||||
async listUsers(): Promise<UserAdminResponseDto[]> {
|
||||
const users = await this.userRepository.getList({ withDeleted: true });
|
||||
return users.map((user) => mapUserAdmin(user));
|
||||
|
||||
@@ -21,6 +21,11 @@ describe(DatabaseService.name, () => {
|
||||
extensionRange = '0.2.x';
|
||||
mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VectorChord);
|
||||
mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange);
|
||||
mocks.database.getSchemaDrift.mockResolvedValue({
|
||||
items: [],
|
||||
asSql: () => [],
|
||||
asHuman: () => [],
|
||||
});
|
||||
|
||||
versionBelowRange = '0.1.0';
|
||||
minVersionInRange = '0.2.0';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import semver from 'semver';
|
||||
import { EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants';
|
||||
import { ErrorMessages, EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { BootstrapEventPriority, DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -124,6 +124,17 @@ export class DatabaseService extends BaseService {
|
||||
const { database } = this.configRepository.getEnv();
|
||||
if (!database.skipMigrations) {
|
||||
await this.databaseRepository.runMigrations();
|
||||
|
||||
this.logger.log('Checking for schema drift');
|
||||
const drift = await this.databaseRepository.getSchemaDrift();
|
||||
if (drift.items.length === 0) {
|
||||
this.logger.log('No schema drift detected');
|
||||
} else {
|
||||
this.logger.warn(`${ErrorMessages.SchemaDrift} or run \`immich-admin schema-check\``);
|
||||
for (const warning of drift.asHuman()) {
|
||||
this.logger.warn(` - ${warning}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
this.databaseRepository.prewarm(VectorIndex.Clip),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { join } from 'node:path';
|
||||
import { ErrorMessages } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import {
|
||||
@@ -114,9 +115,7 @@ export class StorageService extends BaseService {
|
||||
this.logger.log(`Media location changed (from=${previous}, to=${current})`);
|
||||
|
||||
if (!path.startsWith(previous)) {
|
||||
throw new Error(
|
||||
'Detected an inconsistent media location. For more information, see https://docs.immich.app/errors#inconsistent-media-location',
|
||||
);
|
||||
throw new Error(ErrorMessages.InconsistentMediaLocation);
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
|
||||
Reference in New Issue
Block a user