mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	fix(server): run migrations after database checks (#5832)
* run migrations after checks * optional migrations * only run checks in server and e2e * re-add migrations for microservices * refactor * move e2e init * remove assert from migration * update providers * update microservices app service * fixed logging * refactored version check, added unit tests * more version tests * don't use mocks for sut * refactor tests * suggest image only if postgres is 14, 15 or 16 * review suggestions * fixed regexp escape * fix typing * update migration
This commit is contained in:
		
							parent
							
								
									2790a46703
								
							
						
					
					
						commit
						cc2dc12f6c
					
				
							
								
								
									
										134
									
								
								server/src/domain/database/database.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								server/src/domain/database/database.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,134 @@
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { newDatabaseRepositoryMock } from '@test';
 | 
			
		||||
import { Version } from '../domain.constant';
 | 
			
		||||
import { DatabaseExtension, IDatabaseRepository } from '../repositories';
 | 
			
		||||
import { DatabaseService } from './database.service';
 | 
			
		||||
 | 
			
		||||
describe(DatabaseService.name, () => {
 | 
			
		||||
  let sut: DatabaseService;
 | 
			
		||||
  let databaseMock: jest.Mocked<IDatabaseRepository>;
 | 
			
		||||
  let fatalLog: jest.SpyInstance;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    databaseMock = newDatabaseRepositoryMock();
 | 
			
		||||
    fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal');
 | 
			
		||||
 | 
			
		||||
    sut = new DatabaseService(databaseMock);
 | 
			
		||||
 | 
			
		||||
    sut.minVectorsVersion = new Version(0, 1, 1);
 | 
			
		||||
    sut.maxVectorsVersion = new Version(0, 1, 11);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    fatalLog.mockRestore();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should work', () => {
 | 
			
		||||
    expect(sut).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('init', () => {
 | 
			
		||||
    it('should return if minimum supported vectors version is installed', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 1));
 | 
			
		||||
 | 
			
		||||
      await sut.init();
 | 
			
		||||
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS);
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(fatalLog).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return if maximum supported vectors version is installed', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 11));
 | 
			
		||||
 | 
			
		||||
      await sut.init();
 | 
			
		||||
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS);
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(fatalLog).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error if vectors version is not installed even after createVectors', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValueOnce(null);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow('Unexpected: The pgvecto.rs extension is not installed.');
 | 
			
		||||
 | 
			
		||||
      expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error if vectors version is below minimum supported version', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 0, 1));
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0.1.11')/s);
 | 
			
		||||
 | 
			
		||||
      expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error if vectors version is above maximum supported version', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 12));
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(
 | 
			
		||||
        /('DROP EXTENSION IF EXISTS vectors').*('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error if vectors version is a nightly', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 0, 0));
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(
 | 
			
		||||
        /(nightly).*('DROP EXTENSION IF EXISTS vectors').*('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw error if vectors extension could not be created', async () => {
 | 
			
		||||
      databaseMock.createExtension.mockRejectedValueOnce(new Error('Failed to create extension'));
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow('Failed to create extension');
 | 
			
		||||
 | 
			
		||||
      expect(fatalLog).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(fatalLog.mock.calls[0][0]).toMatch(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.11').*(v1\.91\.0)/s);
 | 
			
		||||
      expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
 | 
			
		||||
      expect(databaseMock.runMigrations).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const major of [14, 15, 16]) {
 | 
			
		||||
      it(`should suggest image with postgres ${major} if database is ${major}`, async () => {
 | 
			
		||||
        databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1));
 | 
			
		||||
        databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(major, 0, 0));
 | 
			
		||||
 | 
			
		||||
        await expect(sut.init()).rejects.toThrow(new RegExp(`tensorchord\/pgvecto-rs:pg${major}-v0\\.1\\.11`, 's'));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    it('should not suggest image if postgres version is not in 14, 15 or 16', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1));
 | 
			
		||||
      [13, 17].forEach((major) => databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(major, 0, 0)));
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(/^(?:(?!tensorchord\/pgvecto-rs).)*$/s);
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(/^(?:(?!tensorchord\/pgvecto-rs).)*$/s);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should set the image to the maximum supported version', async () => {
 | 
			
		||||
      databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1));
 | 
			
		||||
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s);
 | 
			
		||||
 | 
			
		||||
      sut.maxVectorsVersion = new Version(0, 1, 12);
 | 
			
		||||
      await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.12')/s);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										69
									
								
								server/src/domain/database/database.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								server/src/domain/database/database.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { QueryFailedError } from 'typeorm';
 | 
			
		||||
import { Version } from '../domain.constant';
 | 
			
		||||
import { DatabaseExtension, IDatabaseRepository } from '../repositories';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DatabaseService {
 | 
			
		||||
  private logger = new ImmichLogger(DatabaseService.name);
 | 
			
		||||
  minVectorsVersion = new Version(0, 1, 1);
 | 
			
		||||
  maxVectorsVersion = new Version(0, 1, 11);
 | 
			
		||||
 | 
			
		||||
  constructor(@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository) {}
 | 
			
		||||
 | 
			
		||||
  async init() {
 | 
			
		||||
    await this.createVectors();
 | 
			
		||||
    await this.assertVectors();
 | 
			
		||||
    await this.databaseRepository.runMigrations();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async assertVectors() {
 | 
			
		||||
    const version = await this.databaseRepository.getExtensionVersion(DatabaseExtension.VECTORS);
 | 
			
		||||
    if (version == null) {
 | 
			
		||||
      throw new Error('Unexpected: The pgvecto.rs extension is not installed.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const image = await this.getVectorsImage();
 | 
			
		||||
    const suggestion = image ? `, such as with the docker image '${image}'` : '';
 | 
			
		||||
 | 
			
		||||
    if (version.isEqual(new Version(0, 0, 0))) {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        `The pgvecto.rs extension version is ${version}, which means it is a nightly release.` +
 | 
			
		||||
          `Please run 'DROP EXTENSION IF EXISTS vectors' and switch to a release version${suggestion}.`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (version.isNewerThan(this.maxVectorsVersion)) {
 | 
			
		||||
      throw new Error(`
 | 
			
		||||
        The pgvecto.rs extension version is ${version} instead of ${this.maxVectorsVersion}.
 | 
			
		||||
        Please run 'DROP EXTENSION IF EXISTS vectors' and switch to ${this.maxVectorsVersion}${suggestion}.`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (version.isOlderThan(this.minVectorsVersion)) {
 | 
			
		||||
      throw new Error(`
 | 
			
		||||
        The pgvecto.rs extension version is ${version}, which is older than the minimum supported version ${this.minVectorsVersion}.
 | 
			
		||||
        Please upgrade to this version or later${suggestion}.`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async createVectors() {
 | 
			
		||||
    await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (err: QueryFailedError) => {
 | 
			
		||||
      const image = await this.getVectorsImage();
 | 
			
		||||
      this.logger.fatal(`
 | 
			
		||||
        Failed to create pgvecto.rs extension.
 | 
			
		||||
        If you have not updated your Postgres instance to a docker image that supports pgvecto.rs (such as '${image}'), please do so.
 | 
			
		||||
        See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0'
 | 
			
		||||
      `);
 | 
			
		||||
      throw err;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getVectorsImage() {
 | 
			
		||||
    const { major } = await this.databaseRepository.getPostgresVersion();
 | 
			
		||||
    if (![14, 15, 16].includes(major)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return `tensorchord/pgvecto-rs:pg${major}-v${this.maxVectorsVersion}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								server/src/domain/database/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/src/domain/database/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export * from './database.service';
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ServerVersion, mimeTypes } from './domain.constant';
 | 
			
		||||
import { Version, mimeTypes } from './domain.constant';
 | 
			
		||||
 | 
			
		||||
describe('mimeTypes', () => {
 | 
			
		||||
  for (const { mimetype, extension } of [
 | 
			
		||||
@ -196,64 +196,76 @@ describe('mimeTypes', () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('ServerVersion', () => {
 | 
			
		||||
  describe('isNewerThan', () => {
 | 
			
		||||
    it('should work on patch versions', () => {
 | 
			
		||||
      expect(new ServerVersion(0, 0, 1).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true);
 | 
			
		||||
      expect(new ServerVersion(1, 72, 1).isNewerThan(new ServerVersion(1, 72, 0))).toBe(true);
 | 
			
		||||
  const tests = [
 | 
			
		||||
    { this: new Version(0, 0, 1), other: new Version(0, 0, 0), expected: 1 },
 | 
			
		||||
    { this: new Version(0, 1, 0), other: new Version(0, 0, 0), expected: 1 },
 | 
			
		||||
    { this: new Version(1, 0, 0), other: new Version(0, 0, 0), expected: 1 },
 | 
			
		||||
    { this: new Version(0, 0, 0), other: new Version(0, 0, 1), expected: -1 },
 | 
			
		||||
    { this: new Version(0, 0, 0), other: new Version(0, 1, 0), expected: -1 },
 | 
			
		||||
    { this: new Version(0, 0, 0), other: new Version(1, 0, 0), expected: -1 },
 | 
			
		||||
    { this: new Version(0, 0, 0), other: new Version(0, 0, 0), expected: 0 },
 | 
			
		||||
    { this: new Version(0, 0, 1), other: new Version(0, 0, 1), expected: 0 },
 | 
			
		||||
    { this: new Version(0, 1, 0), other: new Version(0, 1, 0), expected: 0 },
 | 
			
		||||
    { this: new Version(1, 0, 0), other: new Version(1, 0, 0), expected: 0 },
 | 
			
		||||
    { this: new Version(1, 0), other: new Version(1, 0, 0), expected: 0 },
 | 
			
		||||
    { this: new Version(1, 0), other: new Version(1, 0, 1), expected: -1 },
 | 
			
		||||
    { this: new Version(1, 1), other: new Version(1, 0, 1), expected: 1 },
 | 
			
		||||
    { this: new Version(1), other: new Version(1, 0, 0), expected: 0 },
 | 
			
		||||
    { this: new Version(1), other: new Version(1, 0, 1), expected: -1 },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
      expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(0, 0, 1))).toBe(false);
 | 
			
		||||
      expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 72, 1))).toBe(false);
 | 
			
		||||
  describe('compare', () => {
 | 
			
		||||
    for (const { this: thisVersion, other: otherVersion, expected } of tests) {
 | 
			
		||||
      it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => {
 | 
			
		||||
        expect(thisVersion.compare(otherVersion)).toEqual(expected);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    it('should work on minor versions', () => {
 | 
			
		||||
      expect(new ServerVersion(0, 1, 0).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true);
 | 
			
		||||
      expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 71, 0))).toBe(true);
 | 
			
		||||
      expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 71, 9))).toBe(true);
 | 
			
		||||
 | 
			
		||||
      expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(0, 1, 0))).toBe(false);
 | 
			
		||||
      expect(new ServerVersion(1, 71, 0).isNewerThan(new ServerVersion(1, 72, 0))).toBe(false);
 | 
			
		||||
      expect(new ServerVersion(1, 71, 9).isNewerThan(new ServerVersion(1, 72, 0))).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should work on major versions', () => {
 | 
			
		||||
      expect(new ServerVersion(1, 0, 0).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true);
 | 
			
		||||
      expect(new ServerVersion(2, 0, 0).isNewerThan(new ServerVersion(1, 71, 0))).toBe(true);
 | 
			
		||||
 | 
			
		||||
      expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(1, 0, 0))).toBe(false);
 | 
			
		||||
      expect(new ServerVersion(1, 71, 0).isNewerThan(new ServerVersion(2, 0, 0))).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should work on equal', () => {
 | 
			
		||||
      for (const version of [
 | 
			
		||||
        new ServerVersion(0, 0, 0),
 | 
			
		||||
        new ServerVersion(0, 0, 1),
 | 
			
		||||
        new ServerVersion(0, 1, 1),
 | 
			
		||||
        new ServerVersion(0, 1, 0),
 | 
			
		||||
        new ServerVersion(1, 1, 1),
 | 
			
		||||
        new ServerVersion(1, 0, 0),
 | 
			
		||||
        new ServerVersion(1, 72, 1),
 | 
			
		||||
        new ServerVersion(1, 72, 0),
 | 
			
		||||
        new ServerVersion(1, 73, 9),
 | 
			
		||||
      ]) {
 | 
			
		||||
        expect(version.isNewerThan(version)).toBe(false);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('isOlderThan', () => {
 | 
			
		||||
    for (const { this: thisVersion, other: otherVersion, expected } of tests) {
 | 
			
		||||
      const bool = expected < 0;
 | 
			
		||||
      it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => {
 | 
			
		||||
        expect(thisVersion.isOlderThan(otherVersion)).toEqual(bool);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('isEqual', () => {
 | 
			
		||||
    for (const { this: thisVersion, other: otherVersion, expected } of tests) {
 | 
			
		||||
      const bool = expected === 0;
 | 
			
		||||
      it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => {
 | 
			
		||||
        expect(thisVersion.isEqual(otherVersion)).toEqual(bool);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('isNewerThan', () => {
 | 
			
		||||
    for (const { this: thisVersion, other: otherVersion, expected } of tests) {
 | 
			
		||||
      const bool = expected > 0;
 | 
			
		||||
      it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => {
 | 
			
		||||
        expect(thisVersion.isNewerThan(otherVersion)).toEqual(bool);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('fromString', () => {
 | 
			
		||||
    const tests = [
 | 
			
		||||
      { scenario: 'leading v', value: 'v1.72.2', expected: new ServerVersion(1, 72, 2) },
 | 
			
		||||
      { scenario: 'uppercase v', value: 'V1.72.2', expected: new ServerVersion(1, 72, 2) },
 | 
			
		||||
      { scenario: 'missing v', value: '1.72.2', expected: new ServerVersion(1, 72, 2) },
 | 
			
		||||
      { scenario: 'large patch', value: '1.72.123', expected: new ServerVersion(1, 72, 123) },
 | 
			
		||||
      { scenario: 'large minor', value: '1.123.0', expected: new ServerVersion(1, 123, 0) },
 | 
			
		||||
      { scenario: 'large major', value: '123.0.0', expected: new ServerVersion(123, 0, 0) },
 | 
			
		||||
      { scenario: 'major bump', value: 'v2.0.0', expected: new ServerVersion(2, 0, 0) },
 | 
			
		||||
      { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) },
 | 
			
		||||
      { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) },
 | 
			
		||||
      { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) },
 | 
			
		||||
      { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) },
 | 
			
		||||
      { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) },
 | 
			
		||||
      { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) },
 | 
			
		||||
      { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) },
 | 
			
		||||
      { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) },
 | 
			
		||||
      { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) },
 | 
			
		||||
      { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const { scenario, value, expected } of tests) {
 | 
			
		||||
      it(`should correctly parse ${scenario}`, () => {
 | 
			
		||||
        const actual = ServerVersion.fromString(value);
 | 
			
		||||
        const actual = Version.fromString(value);
 | 
			
		||||
        expect(actual.major).toEqual(expected.major);
 | 
			
		||||
        expect(actual.minor).toEqual(expected.minor);
 | 
			
		||||
        expect(actual.patch).toEqual(expected.patch);
 | 
			
		||||
 | 
			
		||||
@ -6,17 +6,17 @@ import pkg from 'src/../../package.json';
 | 
			
		||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
 | 
			
		||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
 | 
			
		||||
 | 
			
		||||
export interface IServerVersion {
 | 
			
		||||
export interface IVersion {
 | 
			
		||||
  major: number;
 | 
			
		||||
  minor: number;
 | 
			
		||||
  patch: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ServerVersion implements IServerVersion {
 | 
			
		||||
export class Version implements IVersion {
 | 
			
		||||
  constructor(
 | 
			
		||||
    public readonly major: number,
 | 
			
		||||
    public readonly minor: number,
 | 
			
		||||
    public readonly patch: number,
 | 
			
		||||
    public readonly minor: number = 0,
 | 
			
		||||
    public readonly patch: number = 0,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  toString() {
 | 
			
		||||
@ -28,33 +28,45 @@ export class ServerVersion implements IServerVersion {
 | 
			
		||||
    return { major, minor, patch };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static fromString(version: string): ServerVersion {
 | 
			
		||||
    const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
 | 
			
		||||
  static fromString(version: string): Version {
 | 
			
		||||
    const regex = /(?:v)?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[\.-](?<patch>\d+))?/i;
 | 
			
		||||
    const matchResult = version.match(regex);
 | 
			
		||||
    if (matchResult) {
 | 
			
		||||
      const [, major, minor, patch] = matchResult.map(Number);
 | 
			
		||||
      return new ServerVersion(major, minor, patch);
 | 
			
		||||
      const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string };
 | 
			
		||||
      return new Version(Number(major), Number(minor), Number(patch));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error(`Invalid version format: ${version}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isNewerThan(version: ServerVersion): boolean {
 | 
			
		||||
    const equalMajor = this.major === version.major;
 | 
			
		||||
    const equalMinor = this.minor === version.minor;
 | 
			
		||||
  compare(version: Version): number {
 | 
			
		||||
    for (const key of ['major', 'minor', 'patch'] as const) {
 | 
			
		||||
      const diff = this[key] - version[key];
 | 
			
		||||
      if (diff !== 0) {
 | 
			
		||||
        return diff > 0 ? 1 : -1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      this.major > version.major ||
 | 
			
		||||
      (equalMajor && this.minor > version.minor) ||
 | 
			
		||||
      (equalMajor && equalMinor && this.patch > version.patch)
 | 
			
		||||
    );
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isOlderThan(version: Version): boolean {
 | 
			
		||||
    return this.compare(version) < 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isEqual(version: Version): boolean {
 | 
			
		||||
    return this.compare(version) === 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isNewerThan(version: Version): boolean {
 | 
			
		||||
    return this.compare(version) > 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
 | 
			
		||||
export const isDev = process.env.NODE_ENV === 'development';
 | 
			
		||||
 | 
			
		||||
export const serverVersion = ServerVersion.fromString(pkg.version);
 | 
			
		||||
export const serverVersion = Version.fromString(pkg.version);
 | 
			
		||||
 | 
			
		||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import { APIKeyService } from './api-key';
 | 
			
		||||
import { AssetService } from './asset';
 | 
			
		||||
import { AuditService } from './audit';
 | 
			
		||||
import { AuthService } from './auth';
 | 
			
		||||
import { DatabaseService } from './database';
 | 
			
		||||
import { JobService } from './job';
 | 
			
		||||
import { LibraryService } from './library';
 | 
			
		||||
import { MediaService } from './media';
 | 
			
		||||
@ -29,6 +30,7 @@ const providers: Provider[] = [
 | 
			
		||||
  AssetService,
 | 
			
		||||
  AuditService,
 | 
			
		||||
  AuthService,
 | 
			
		||||
  DatabaseService,
 | 
			
		||||
  JobService,
 | 
			
		||||
  MediaService,
 | 
			
		||||
  MetadataService,
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ export * from './api-key';
 | 
			
		||||
export * from './asset';
 | 
			
		||||
export * from './audit';
 | 
			
		||||
export * from './auth';
 | 
			
		||||
export * from './database';
 | 
			
		||||
export * from './domain.config';
 | 
			
		||||
export * from './domain.constant';
 | 
			
		||||
export * from './domain.module';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								server/src/domain/repositories/database.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/src/domain/repositories/database.repository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
import { Version } from '../domain.constant';
 | 
			
		||||
 | 
			
		||||
export enum DatabaseExtension {
 | 
			
		||||
  CUBE = 'cube',
 | 
			
		||||
  EARTH_DISTANCE = 'earthdistance',
 | 
			
		||||
  VECTORS = 'vectors',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const IDatabaseRepository = 'IDatabaseRepository';
 | 
			
		||||
 | 
			
		||||
export interface IDatabaseRepository {
 | 
			
		||||
  getExtensionVersion(extName: string): Promise<Version | null>;
 | 
			
		||||
  getPostgresVersion(): Promise<Version>;
 | 
			
		||||
  createExtension(extension: DatabaseExtension): Promise<void>;
 | 
			
		||||
  runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
@ -6,6 +6,7 @@ export * from './asset.repository';
 | 
			
		||||
export * from './audit.repository';
 | 
			
		||||
export * from './communication.repository';
 | 
			
		||||
export * from './crypto.repository';
 | 
			
		||||
export * from './database.repository';
 | 
			
		||||
export * from './job.repository';
 | 
			
		||||
export * from './library.repository';
 | 
			
		||||
export * from './machine-learning.repository';
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { FeatureFlags, IServerVersion } from '@app/domain';
 | 
			
		||||
import { FeatureFlags, IVersion } from '@app/domain';
 | 
			
		||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
 | 
			
		||||
import { SystemConfigThemeDto } from '../system-config/dto/system-config-theme.dto';
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ export class ServerInfoResponseDto {
 | 
			
		||||
  diskUsagePercentage!: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ServerVersionResponseDto implements IServerVersion {
 | 
			
		||||
export class ServerVersionResponseDto implements IVersion {
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
  major!: number;
 | 
			
		||||
  @ApiProperty({ type: 'integer' })
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DateTime } from 'luxon';
 | 
			
		||||
import { ServerVersion, isDev, mimeTypes, serverVersion } from '../domain.constant';
 | 
			
		||||
import { Version, isDev, mimeTypes, serverVersion } from '../domain.constant';
 | 
			
		||||
import { asHumanReadable } from '../domain.util';
 | 
			
		||||
import {
 | 
			
		||||
  ClientEvent,
 | 
			
		||||
@ -138,7 +138,7 @@ export class ServerInfoService {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const githubRelease = await this.repository.getGitHubRelease();
 | 
			
		||||
      const githubVersion = ServerVersion.fromString(githubRelease.tag_name);
 | 
			
		||||
      const githubVersion = Version.fromString(githubRelease.tag_name);
 | 
			
		||||
      const publishedAt = new Date(githubRelease.published_at);
 | 
			
		||||
      this.releaseVersion = githubVersion;
 | 
			
		||||
      this.releaseVersionCheckedAt = DateTime.now();
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  AuthService,
 | 
			
		||||
  DatabaseService,
 | 
			
		||||
  JobService,
 | 
			
		||||
  ONE_HOUR,
 | 
			
		||||
  OpenGraphTags,
 | 
			
		||||
@ -46,6 +47,7 @@ export class AppService {
 | 
			
		||||
    private serverService: ServerInfoService,
 | 
			
		||||
    private sharedLinkService: SharedLinkService,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
    private databaseService: DatabaseService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @Interval(ONE_HOUR.as('milliseconds'))
 | 
			
		||||
@ -59,6 +61,7 @@ export class AppService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async init() {
 | 
			
		||||
    await this.databaseService.init();
 | 
			
		||||
    await this.configService.init();
 | 
			
		||||
    this.storageService.init();
 | 
			
		||||
    await this.serverService.handleVersionCheck();
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { envName, isDev, serverVersion } from '@app/domain';
 | 
			
		||||
import { WebSocketAdapter, databaseChecks } from '@app/infra';
 | 
			
		||||
import { WebSocketAdapter } from '@app/infra';
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { NestFactory } from '@nestjs/core';
 | 
			
		||||
import { NestExpressApplication } from '@nestjs/platform-express';
 | 
			
		||||
@ -31,8 +31,6 @@ export async function bootstrap() {
 | 
			
		||||
  app.useStaticAssets('www');
 | 
			
		||||
  app.use(app.get(AppService).ssr(excludePaths));
 | 
			
		||||
 | 
			
		||||
  await databaseChecks();
 | 
			
		||||
 | 
			
		||||
  const server = await app.listen(port);
 | 
			
		||||
  server.requestTimeout = 30 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { DataSource, QueryRunner } from 'typeorm';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
 | 
			
		||||
 | 
			
		||||
const url = process.env.DB_URL;
 | 
			
		||||
@ -18,58 +18,10 @@ export const databaseConfig: PostgresConnectionOptions = {
 | 
			
		||||
  synchronize: false,
 | 
			
		||||
  migrations: [__dirname + '/migrations/*.{js,ts}'],
 | 
			
		||||
  subscribers: [__dirname + '/subscribers/*.{js,ts}'],
 | 
			
		||||
  migrationsRun: true,
 | 
			
		||||
  migrationsRun: false,
 | 
			
		||||
  connectTimeoutMS: 10000, // 10 seconds
 | 
			
		||||
  ...urlOrParts,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// this export is used by TypeORM commands in package.json#scripts
 | 
			
		||||
export const dataSource = new DataSource(databaseConfig);
 | 
			
		||||
 | 
			
		||||
export async function databaseChecks() {
 | 
			
		||||
  if (!dataSource.isInitialized) {
 | 
			
		||||
    await dataSource.initialize();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await assertVectors(dataSource);
 | 
			
		||||
  await enablePrefilter(dataSource);
 | 
			
		||||
  await dataSource.runMigrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function enablePrefilter(runner: DataSource | QueryRunner) {
 | 
			
		||||
  await runner.query(`SET vectors.enable_prefilter = on`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getExtensionVersion(extName: string, runner: DataSource | QueryRunner): Promise<string | null> {
 | 
			
		||||
  const res = await runner.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extName]);
 | 
			
		||||
  return res[0]?.['extversion'] ?? null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getPostgresVersion(runner: DataSource | QueryRunner): Promise<string> {
 | 
			
		||||
  const res = await runner.query(`SHOW server_version`);
 | 
			
		||||
  return res[0]['server_version'].split('.')[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function assertVectors(runner: DataSource | QueryRunner) {
 | 
			
		||||
  const postgresVersion = await getPostgresVersion(runner);
 | 
			
		||||
  const expected = ['0.1.1', '0.1.11'];
 | 
			
		||||
  const image = `tensorchord/pgvecto-rs:pg${postgresVersion}-v${expected[expected.length - 1]}`;
 | 
			
		||||
 | 
			
		||||
  await runner.query('CREATE EXTENSION IF NOT EXISTS vectors').catch((err) => {
 | 
			
		||||
    console.error(
 | 
			
		||||
      'Failed to create pgvecto.rs extension. ' +
 | 
			
		||||
        `If you have not updated your Postgres instance to an image that supports pgvecto.rs (such as '${image}'), please do so. ` +
 | 
			
		||||
        'See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0',
 | 
			
		||||
    );
 | 
			
		||||
    throw err;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const version = await getExtensionVersion('vectors', runner);
 | 
			
		||||
  if (version != null && !expected.includes(version)) {
 | 
			
		||||
    throw new Error(
 | 
			
		||||
      `The pgvecto.rs extension version is ${version} instead of the expected version ${
 | 
			
		||||
        expected[expected.length - 1]
 | 
			
		||||
      }.` + `If you're using the 'latest' tag, please switch to '${image}'.`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import {
 | 
			
		||||
  IAuditRepository,
 | 
			
		||||
  ICommunicationRepository,
 | 
			
		||||
  ICryptoRepository,
 | 
			
		||||
  IDatabaseRepository,
 | 
			
		||||
  IJobRepository,
 | 
			
		||||
  IKeyRepository,
 | 
			
		||||
  ILibraryRepository,
 | 
			
		||||
@ -43,6 +44,7 @@ import {
 | 
			
		||||
  AuditRepository,
 | 
			
		||||
  CommunicationRepository,
 | 
			
		||||
  CryptoRepository,
 | 
			
		||||
  DatabaseRepository,
 | 
			
		||||
  FilesystemProvider,
 | 
			
		||||
  JobRepository,
 | 
			
		||||
  LibraryRepository,
 | 
			
		||||
@ -70,6 +72,7 @@ const providers: Provider[] = [
 | 
			
		||||
  { provide: IAuditRepository, useClass: AuditRepository },
 | 
			
		||||
  { provide: ICommunicationRepository, useClass: CommunicationRepository },
 | 
			
		||||
  { provide: ICryptoRepository, useClass: CryptoRepository },
 | 
			
		||||
  { provide: IDatabaseRepository, useClass: DatabaseRepository },
 | 
			
		||||
  { provide: IJobRepository, useClass: JobRepository },
 | 
			
		||||
  { provide: ILibraryRepository, useClass: LibraryRepository },
 | 
			
		||||
  { provide: IKeyRepository, useClass: ApiKeyRepository },
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { LogLevel } from './entities';
 | 
			
		||||
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
 | 
			
		||||
 | 
			
		||||
export class ImmichLogger extends ConsoleLogger {
 | 
			
		||||
  private static logLevels: LogLevel[] = [];
 | 
			
		||||
  private static logLevels: LogLevel[] = [LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
 | 
			
		||||
 | 
			
		||||
  constructor(context: string) {
 | 
			
		||||
    super(context);
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,11 @@
 | 
			
		||||
import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
import { assertVectors } from '../database.config';
 | 
			
		||||
 | 
			
		||||
export class UsePgVectors1700713871511 implements MigrationInterface {
 | 
			
		||||
  name = 'UsePgVectors1700713871511';
 | 
			
		||||
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await assertVectors(queryRunner);
 | 
			
		||||
 | 
			
		||||
    await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS vectors`);
 | 
			
		||||
    const faceDimQuery = await queryRunner.query(`
 | 
			
		||||
        SELECT CARDINALITY(embedding::real[]) as dimsize
 | 
			
		||||
        FROM asset_faces
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								server/src/infra/repositories/database.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								server/src/infra/repositories/database.repository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import { DatabaseExtension, IDatabaseRepository, Version } from '@app/domain';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectDataSource } from '@nestjs/typeorm';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DatabaseRepository implements IDatabaseRepository {
 | 
			
		||||
  constructor(@InjectDataSource() private dataSource: DataSource) {}
 | 
			
		||||
 | 
			
		||||
  async getExtensionVersion(extension: DatabaseExtension): Promise<Version | null> {
 | 
			
		||||
    const res = await this.dataSource.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extension]);
 | 
			
		||||
    const version = res[0]?.['extversion'];
 | 
			
		||||
    return version == null ? null : Version.fromString(version);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getPostgresVersion(): Promise<Version> {
 | 
			
		||||
    const res = await this.dataSource.query(`SHOW server_version`);
 | 
			
		||||
    return Version.fromString(res[0]['server_version']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createExtension(extension: DatabaseExtension): Promise<void> {
 | 
			
		||||
    await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void> {
 | 
			
		||||
    await this.dataSource.runMigrations(options);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -6,6 +6,7 @@ export * from './asset.repository';
 | 
			
		||||
export * from './audit.repository';
 | 
			
		||||
export * from './communication.repository';
 | 
			
		||||
export * from './crypto.repository';
 | 
			
		||||
export * from './database.repository';
 | 
			
		||||
export * from './filesystem.provider';
 | 
			
		||||
export * from './job.repository';
 | 
			
		||||
export * from './library.repository';
 | 
			
		||||
 | 
			
		||||
@ -52,6 +52,7 @@ export class SmartInfoRepository implements ISmartInfoRepository {
 | 
			
		||||
    let results: AssetEntity[] = [];
 | 
			
		||||
    await this.assetRepository.manager.transaction(async (manager) => {
 | 
			
		||||
      await manager.query(`SET LOCAL vectors.k = '${numResults}'`);
 | 
			
		||||
      await manager.query(`SET LOCAL vectors.enable_prefilter = on`);
 | 
			
		||||
      results = await manager
 | 
			
		||||
        .createQueryBuilder(AssetEntity, 'a')
 | 
			
		||||
        .innerJoin('a.smartSearch', 's')
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@
 | 
			
		||||
START TRANSACTION
 | 
			
		||||
SET
 | 
			
		||||
  LOCAL vectors.k = '100'
 | 
			
		||||
SET
 | 
			
		||||
  LOCAL vectors.enable_prefilter = on
 | 
			
		||||
SELECT
 | 
			
		||||
  "a"."id" AS "a_id",
 | 
			
		||||
  "a"."deviceAssetId" AS "a_deviceAssetId",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  AssetService,
 | 
			
		||||
  AuditService,
 | 
			
		||||
  DatabaseService,
 | 
			
		||||
  IDeleteFilesJob,
 | 
			
		||||
  JobName,
 | 
			
		||||
  JobService,
 | 
			
		||||
@ -31,9 +32,11 @@ export class AppService {
 | 
			
		||||
    private storageTemplateService: StorageTemplateService,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
    private userService: UserService,
 | 
			
		||||
    private databaseService: DatabaseService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async init() {
 | 
			
		||||
    await this.databaseService.init();
 | 
			
		||||
    await this.configService.init();
 | 
			
		||||
    await this.jobService.registerHandlers({
 | 
			
		||||
      [JobName.ASSET_DELETION]: (data) => this.assetService.handleAssetDeletion(data),
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { envName, serverVersion } from '@app/domain';
 | 
			
		||||
import { WebSocketAdapter, databaseChecks } from '@app/infra';
 | 
			
		||||
import { WebSocketAdapter } from '@app/infra';
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { NestFactory } from '@nestjs/core';
 | 
			
		||||
import { MicroservicesModule } from './microservices.module';
 | 
			
		||||
@ -12,7 +12,6 @@ export async function bootstrap() {
 | 
			
		||||
 | 
			
		||||
  app.useLogger(app.get(ImmichLogger));
 | 
			
		||||
  app.useWebSocketAdapter(new WebSocketAdapter(app));
 | 
			
		||||
  await databaseChecks();
 | 
			
		||||
 | 
			
		||||
  await app.listen(port);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								server/test/repositories/database.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/test/repositories/database.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
import { IDatabaseRepository, Version } from '@app/domain';
 | 
			
		||||
 | 
			
		||||
export const newDatabaseRepositoryMock = (): jest.Mocked<IDatabaseRepository> => {
 | 
			
		||||
  return {
 | 
			
		||||
    getExtensionVersion: jest.fn(),
 | 
			
		||||
    getPostgresVersion: jest.fn().mockResolvedValue(new Version(14, 0, 0)),
 | 
			
		||||
    createExtension: jest.fn().mockImplementation(() => Promise.resolve()),
 | 
			
		||||
    runMigrations: jest.fn(),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -5,6 +5,7 @@ export * from './asset.repository.mock';
 | 
			
		||||
export * from './audit.repository.mock';
 | 
			
		||||
export * from './communication.repository.mock';
 | 
			
		||||
export * from './crypto.repository.mock';
 | 
			
		||||
export * from './database.repository.mock';
 | 
			
		||||
export * from './job.repository.mock';
 | 
			
		||||
export * from './library.repository.mock';
 | 
			
		||||
export * from './machine-learning.repository.mock';
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { AssetCreate, IJobRepository, JobItem, JobItemHandler, LibraryResponseDto, QueueName } from '@app/domain';
 | 
			
		||||
import { AppModule } from '@app/immich';
 | 
			
		||||
import { dataSource, databaseChecks } from '@app/infra';
 | 
			
		||||
import { dataSource } from '@app/infra';
 | 
			
		||||
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
 | 
			
		||||
import { INestApplication } from '@nestjs/common';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
@ -24,7 +24,9 @@ export interface ResetOptions {
 | 
			
		||||
}
 | 
			
		||||
export const db = {
 | 
			
		||||
  reset: async (options?: ResetOptions) => {
 | 
			
		||||
    await databaseChecks();
 | 
			
		||||
    if (!dataSource.isInitialized) {
 | 
			
		||||
      await dataSource.initialize();
 | 
			
		||||
    }
 | 
			
		||||
    await dataSource.transaction(async (em) => {
 | 
			
		||||
      const entities = options?.entities || [];
 | 
			
		||||
      const tableNames =
 | 
			
		||||
@ -87,10 +89,7 @@ export const testApp = {
 | 
			
		||||
 | 
			
		||||
    app = await moduleFixture.createNestApplication().init();
 | 
			
		||||
    await app.listen(0);
 | 
			
		||||
 | 
			
		||||
    if (jobs) {
 | 
			
		||||
    await app.get(AppService).init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const port = app.getHttpServer().address().port;
 | 
			
		||||
    const protocol = app instanceof Server ? 'https' : 'http';
 | 
			
		||||
@ -99,6 +98,7 @@ export const testApp = {
 | 
			
		||||
    return app;
 | 
			
		||||
  },
 | 
			
		||||
  reset: async (options?: ResetOptions) => {
 | 
			
		||||
    await app.get(AppService).init();
 | 
			
		||||
    await db.reset(options);
 | 
			
		||||
  },
 | 
			
		||||
  teardown: async () => {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user