chore(server): eslint await-thenable (#7545)

* await-thenable

* fix library watchers

* moar eslint

* fix test

* fix typo

* try to remove check void return

* fix checksVoidReturn

* move to domain utils

* remove eslint ignores

* chore: cleanup types

* chore: use logger

* fix: e2e

---------

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Jonathan Jogenfors 2024-03-05 23:23:06 +01:00 committed by GitHub
parent 972d5a3411
commit 5d377e5b0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 133 additions and 110 deletions

View File

@ -25,6 +25,12 @@ module.exports = {
'unicorn/prefer-top-level-await': 'off', 'unicorn/prefer-top-level-await': 'off',
'unicorn/prefer-event-target': 'off', 'unicorn/prefer-event-target': 'off',
'unicorn/no-thenable': 'off', 'unicorn/no-thenable': 'off',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
// Note: you must disable the base rule as it can report incorrect errors
'require-await': 'off',
'@typescript-eslint/require-await': 'error',
curly: 2, curly: 2,
'prettier/prettier': 0, 'prettier/prettier': 0,
}, },

View File

@ -208,7 +208,7 @@ describe(`Library watcher (e2e)`, () => {
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
}); });
it('should use an updated import paths', async () => { it('should use an updated import path', async () => {
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, { recursive: true });
await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [ await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [

View File

@ -11,7 +11,7 @@ describe(ActivityService.name, () => {
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let activityMock: jest.Mocked<IActivityRepository>; let activityMock: jest.Mocked<IActivityRepository>;
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
activityMock = newActivityRepositoryMock(); activityMock = newActivityRepositoryMock();

View File

@ -23,7 +23,7 @@ describe(AlbumService.name, () => {
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let userMock: jest.Mocked<IUserRepository>; let userMock: jest.Mocked<IUserRepository>;
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
albumMock = newAlbumRepositoryMock(); albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();

View File

@ -8,7 +8,7 @@ describe(APIKeyService.name, () => {
let keyMock: jest.Mocked<IKeyRepository>; let keyMock: jest.Mocked<IKeyRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>; let cryptoMock: jest.Mocked<ICryptoRepository>;
beforeEach(async () => { beforeEach(() => {
cryptoMock = newCryptoRepositoryMock(); cryptoMock = newCryptoRepositoryMock();
keyMock = newKeyRepositoryMock(); keyMock = newKeyRepositoryMock();
sut = new APIKeyService(cryptoMock, keyMock); sut = new APIKeyService(cryptoMock, keyMock);

View File

@ -169,7 +169,7 @@ describe(AssetService.name, () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
communicationMock = newCommunicationRepositoryMock(); communicationMock = newCommunicationRepositoryMock();

View File

@ -31,7 +31,7 @@ describe(AuditService.name, () => {
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>; let userMock: jest.Mocked<IUserRepository>;
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
cryptoMock = newCryptoRepositoryMock(); cryptoMock = newCryptoRepositoryMock();

View File

@ -74,7 +74,7 @@ describe('AuthService', () => {
let callbackMock: jest.Mock; let callbackMock: jest.Mock;
let userinfoMock: jest.Mock; let userinfoMock: jest.Mock;
beforeEach(async () => { beforeEach(() => {
callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' }); callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' });
userinfoMock = jest.fn().mockResolvedValue({ sub, email }); userinfoMock = jest.fn().mockResolvedValue({ sub, email });

View File

@ -13,7 +13,7 @@ describe(DatabaseService.name, () => {
let sut: DatabaseService; let sut: DatabaseService;
let databaseMock: jest.Mocked<IDatabaseRepository>; let databaseMock: jest.Mocked<IDatabaseRepository>;
beforeEach(async () => { beforeEach(() => {
databaseMock = newDatabaseRepositoryMock(); databaseMock = newDatabaseRepositoryMock();
sut = new DatabaseService(databaseMock); sut = new DatabaseService(databaseMock);
@ -31,7 +31,7 @@ describe(DatabaseService.name, () => {
let errorLog: jest.SpyInstance; let errorLog: jest.SpyInstance;
let warnLog: jest.SpyInstance; let warnLog: jest.SpyInstance;
beforeEach(async () => { beforeEach(() => {
fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal'); fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal');
errorLog = jest.spyOn(ImmichLogger.prototype, 'error'); errorLog = jest.spyOn(ImmichLogger.prototype, 'error');
warnLog = jest.spyOn(ImmichLogger.prototype, 'warn'); warnLog = jest.spyOn(ImmichLogger.prototype, 'warn');

View File

@ -1,6 +1,5 @@
import { ImmichLogger } from '@app/infra/logger'; import { ImmichLogger } from '@app/infra/logger';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { QueryFailedError } from 'typeorm';
import { Version, VersionType } from '../domain.constant'; import { Version, VersionType } from '../domain.constant';
import { import {
DatabaseExtension, DatabaseExtension,
@ -61,7 +60,9 @@ export class DatabaseService {
} }
private async createVectorExtension() { private async createVectorExtension() {
await this.databaseRepository.createExtension(this.vectorExt).catch(async (error: QueryFailedError) => { try {
await this.databaseRepository.createExtension(this.vectorExt);
} catch (error) {
const otherExt = const otherExt =
this.vectorExt === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; this.vectorExt === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS;
this.logger.fatal(` this.logger.fatal(`
@ -78,7 +79,7 @@ export class DatabaseService {
In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup. In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.
`); `);
throw error; throw error;
}); }
} }
private async updateVectorExtension() { private async updateVectorExtension() {

View File

@ -1,3 +1,4 @@
import { ImmichLogger } from '@app/infra/logger';
import { applyDecorators } from '@nestjs/common'; import { applyDecorators } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
@ -157,7 +158,7 @@ export type Paginated<T> = Promise<PaginationResult<T>>;
export async function* usePagination<T>( export async function* usePagination<T>(
pageSize: number, pageSize: number,
getNextPage: (pagination: PaginationOptions) => Paginated<T>, getNextPage: (pagination: PaginationOptions) => PaginationResult<T> | Paginated<T>,
) { ) {
let hasNextPage = true; let hasNextPage = true;
@ -252,3 +253,7 @@ export const setIsSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => {
export const setIsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => { export const setIsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => {
return setA.size === setB.size && setIsSuperset(setA, setB); return setA.size === setB.size && setIsSuperset(setA, setB);
}; };
export const handlePromiseError = <T>(promise: Promise<T>, logger: ImmichLogger): void => {
promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
};

View File

@ -34,7 +34,7 @@ describe(DownloadService.name, () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();

View File

@ -114,9 +114,7 @@ export class DownloadService {
const assetIds = dto.assetIds; const assetIds = dto.assetIds;
await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds);
const assets = await this.assetRepository.getByIds(assetIds); const assets = await this.assetRepository.getByIds(assetIds);
return (async function* () { return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
yield assets;
})();
} }
if (dto.albumId) { if (dto.albumId) {

View File

@ -37,7 +37,7 @@ describe(JobService.name, () => {
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let personMock: jest.Mocked<IPersonRepository>; let personMock: jest.Mocked<IPersonRepository>;
beforeEach(async () => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
communicationMock = newCommunicationRepositoryMock(); communicationMock = newCommunicationRepositoryMock();

View File

@ -17,6 +17,7 @@ import {
systemConfigStub, systemConfigStub,
userStub, userStub,
} from '@test'; } from '@test';
import { when } from 'jest-when';
import { Stats } from 'node:fs'; import { Stats } from 'node:fs';
import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job';
import { import {
@ -55,7 +56,7 @@ describe(LibraryService.name, () => {
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();
// Always validate owner access for library. // Always validate owner access for library.
accessMock.library.checkOwnerAccess.mockImplementation(async (_, libraryIds) => libraryIds); accessMock.library.checkOwnerAccess.mockImplementation((_, libraryIds) => Promise.resolve(libraryIds));
sut = new LibraryService( sut = new LibraryService(
accessMock, accessMock,
@ -106,19 +107,13 @@ describe(LibraryService.name, () => {
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
libraryMock.get.mockImplementation(async (id) => { when(libraryMock.get)
switch (id) { .calledWith(libraryStub.externalLibraryWithImportPaths1.id)
case libraryStub.externalLibraryWithImportPaths1.id: { .mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
return libraryStub.externalLibraryWithImportPaths1;
} when(libraryMock.get)
case libraryStub.externalLibraryWithImportPaths2.id: { .calledWith(libraryStub.externalLibraryWithImportPaths2.id)
return libraryStub.externalLibraryWithImportPaths2; .mockResolvedValue(libraryStub.externalLibraryWithImportPaths2);
}
default: {
return null;
}
}
});
await sut.init(); await sut.init();
@ -1278,19 +1273,13 @@ describe(LibraryService.name, () => {
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
libraryMock.get.mockImplementation(async (id) => { when(libraryMock.get)
switch (id) { .calledWith(libraryStub.externalLibraryWithImportPaths1.id)
case libraryStub.externalLibraryWithImportPaths1.id: { .mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
return libraryStub.externalLibraryWithImportPaths1;
} when(libraryMock.get)
case libraryStub.externalLibraryWithImportPaths2.id: { .calledWith(libraryStub.externalLibraryWithImportPaths2.id)
return libraryStub.externalLibraryWithImportPaths2; .mockResolvedValue(libraryStub.externalLibraryWithImportPaths2);
}
default: {
return null;
}
}
});
const mockClose = jest.fn(); const mockClose = jest.fn();
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
@ -1304,9 +1293,8 @@ describe(LibraryService.name, () => {
describe('handleDeleteLibrary', () => { describe('handleDeleteLibrary', () => {
it('should not delete a nonexistent library', async () => { it('should not delete a nonexistent library', async () => {
libraryMock.get.mockImplementation(async () => { libraryMock.get.mockResolvedValue(null);
return null;
});
libraryMock.getAssetIds.mockResolvedValue([]); libraryMock.getAssetIds.mockResolvedValue([]);
libraryMock.delete.mockImplementation(async () => {}); libraryMock.delete.mockImplementation(async () => {});

View File

@ -9,7 +9,7 @@ import picomatch from 'picomatch';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AuthDto } from '../auth'; import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant'; import { mimeTypes } from '../domain.constant';
import { usePagination, validateCronExpression } from '../domain.util'; import { handlePromiseError, usePagination, validateCronExpression } from '../domain.util';
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
import { import {
@ -43,7 +43,7 @@ export class LibraryService extends EventEmitter {
private access: AccessCore; private access: AccessCore;
private configCore: SystemConfigCore; private configCore: SystemConfigCore;
private watchLibraries = false; private watchLibraries = false;
private watchers: Record<string, () => void> = {}; private watchers: Record<string, () => Promise<void>> = {};
constructor( constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAccessRepository) accessRepository: IAccessRepository,
@ -73,7 +73,11 @@ export class LibraryService extends EventEmitter {
this.jobRepository.addCronJob( this.jobRepository.addCronJob(
'libraryScan', 'libraryScan',
scan.cronExpression, scan.cronExpression,
() => this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }), () =>
handlePromiseError(
this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }),
this.logger,
),
scan.enabled, scan.enabled,
); );
@ -81,12 +85,12 @@ export class LibraryService extends EventEmitter {
await this.watchAll(); await this.watchAll();
} }
this.configCore.config$.subscribe(async ({ library }) => { this.configCore.config$.subscribe(({ library }) => {
this.jobRepository.updateCronJob('libraryScan', library.scan.cronExpression, library.scan.enabled); this.jobRepository.updateCronJob('libraryScan', library.scan.cronExpression, library.scan.enabled);
if (library.watch.enabled !== this.watchLibraries) { if (library.watch.enabled !== this.watchLibraries) {
this.watchLibraries = library.watch.enabled; this.watchLibraries = library.watch.enabled;
await (this.watchLibraries ? this.watchAll() : this.unwatchAll()); handlePromiseError(this.watchLibraries ? this.watchAll() : this.unwatchAll(), this.logger);
} }
}); });
} }
@ -124,28 +128,37 @@ export class LibraryService extends EventEmitter {
}, },
{ {
onReady: () => _resolve(), onReady: () => _resolve(),
onAdd: async (path) => { onAdd: (path) => {
this.logger.debug(`File add event received for ${path} in library ${library.id}}`); const handler = async () => {
if (matcher(path)) { this.logger.debug(`File add event received for ${path} in library ${library.id}}`);
await this.scanAssets(library.id, [path], library.ownerId, false); if (matcher(path)) {
} await this.scanAssets(library.id, [path], library.ownerId, false);
this.emit('add', path); }
this.emit('add', path);
};
return handlePromiseError(handler(), this.logger);
}, },
onChange: async (path) => { onChange: (path) => {
this.logger.debug(`Detected file change for ${path} in library ${library.id}`); const handler = async () => {
if (matcher(path)) { this.logger.debug(`Detected file change for ${path} in library ${library.id}`);
// Note: if the changed file was not previously imported, it will be imported now. if (matcher(path)) {
await this.scanAssets(library.id, [path], library.ownerId, false); // Note: if the changed file was not previously imported, it will be imported now.
} await this.scanAssets(library.id, [path], library.ownerId, false);
this.emit('change', path); }
this.emit('change', path);
};
return handlePromiseError(handler(), this.logger);
}, },
onUnlink: async (path) => { onUnlink: (path) => {
this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); const handler = async () => {
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`);
if (asset && matcher(path)) { const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
await this.assetRepository.save({ id: asset.id, isOffline: true }); if (asset && matcher(path)) {
} await this.assetRepository.save({ id: asset.id, isOffline: true });
this.emit('unlink', path); }
this.emit('unlink', path);
};
return handlePromiseError(handler(), this.logger);
}, },
onError: (error) => { onError: (error) => {
// TODO: should we log, or throw an exception? // TODO: should we log, or throw an exception?

View File

@ -48,7 +48,7 @@ describe(MediaService.name, () => {
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>; let cryptoMock: jest.Mocked<ICryptoRepository>;
beforeEach(async () => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();

View File

@ -56,7 +56,7 @@ describe(MetadataService.name, () => {
let databaseMock: jest.Mocked<IDatabaseRepository>; let databaseMock: jest.Mocked<IDatabaseRepository>;
let sut: MetadataService; let sut: MetadataService;
beforeEach(async () => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();

View File

@ -7,7 +7,7 @@ import _ from 'lodash';
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import { constants } from 'node:fs/promises'; import { constants } from 'node:fs/promises';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { usePagination } from '../domain.util'; import { handlePromiseError, usePagination } from '../domain.util';
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
import { import {
ClientEvent, ClientEvent,
@ -124,7 +124,7 @@ export class MetadataService {
async init() { async init() {
if (!this.subscription) { if (!this.subscription) {
this.subscription = this.configCore.config$.subscribe(() => this.init()); this.subscription = this.configCore.config$.subscribe(() => handlePromiseError(this.init(), this.logger));
} }
const { reverseGeocoding } = await this.configCore.getConfig(); const { reverseGeocoding } = await this.configCore.getConfig();

View File

@ -49,7 +49,7 @@ describe(PartnerService.name, () => {
let partnerMock: jest.Mocked<IPartnerRepository>; let partnerMock: jest.Mocked<IPartnerRepository>;
let accessMock: jest.Mocked<IAccessRepository>; let accessMock: jest.Mocked<IAccessRepository>;
beforeEach(async () => { beforeEach(() => {
partnerMock = newPartnerRepositoryMock(); partnerMock = newPartnerRepositoryMock();
sut = new PartnerService(partnerMock, accessMock); sut = new PartnerService(partnerMock, accessMock);
}); });

View File

@ -80,7 +80,7 @@ describe(PersonService.name, () => {
let cryptoMock: jest.Mocked<ICryptoRepository>; let cryptoMock: jest.Mocked<ICryptoRepository>;
let sut: PersonService; let sut: PersonService;
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();

View File

@ -34,7 +34,7 @@ export interface ClientEventMap {
[ClientEvent.NEW_RELEASE]: ReleaseNotification; [ClientEvent.NEW_RELEASE]: ReleaseNotification;
} }
export type OnConnectCallback = (userId: string) => Promise<void>; export type OnConnectCallback = (userId: string) => void | Promise<void>;
export type OnServerEventCallback = () => Promise<void>; export type OnServerEventCallback = () => Promise<void>;
export interface ICommunicationRepository { export interface ICommunicationRepository {

View File

@ -47,6 +47,6 @@ export interface IStorageRepository {
crawl(crawlOptions: CrawlOptionsDto): Promise<string[]>; crawl(crawlOptions: CrawlOptionsDto): Promise<string[]>;
copyFile(source: string, target: string): Promise<void>; copyFile(source: string, target: string): Promise<void>;
rename(source: string, target: string): Promise<void>; rename(source: string, target: string): Promise<void>;
watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>): () => void; watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>): () => Promise<void>;
utimes(filepath: string, atime: Date, mtime: Date): Promise<void>; utimes(filepath: string, atime: Date, mtime: Date): Promise<void>;
} }

View File

@ -181,7 +181,7 @@ export class SearchService {
return userIds; return userIds;
} }
private async mapResponse(assets: AssetEntity[], nextPage: string | null): Promise<SearchResponseDto> { private mapResponse(assets: AssetEntity[], nextPage: string | null): SearchResponseDto {
return { return {
albums: { total: 0, count: 0, items: [], facets: [] }, albums: { total: 0, count: 0, items: [], facets: [] },
assets: { assets: {

View File

@ -170,7 +170,7 @@ export class ServerInfoService {
return true; return true;
} }
private async handleConnect(userId: string) { private handleConnect(userId: string) {
this.communicationRepository.send(ClientEvent.SERVER_VERSION, userId, serverVersion); this.communicationRepository.send(ClientEvent.SERVER_VERSION, userId, serverVersion);
this.newReleaseNotification(userId); this.newReleaseNotification(userId);
} }

View File

@ -22,7 +22,7 @@ describe(SharedLinkService.name, () => {
let cryptoMock: jest.Mocked<ICryptoRepository>; let cryptoMock: jest.Mocked<ICryptoRepository>;
let shareMock: jest.Mocked<ISharedLinkRepository>; let shareMock: jest.Mocked<ISharedLinkRepository>;
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
cryptoMock = newCryptoRepositoryMock(); cryptoMock = newCryptoRepositoryMock();
shareMock = newSharedLinkRepositoryMock(); shareMock = newSharedLinkRepositoryMock();

View File

@ -35,7 +35,7 @@ describe(SmartInfoService.name, () => {
let machineMock: jest.Mocked<IMachineLearningRepository>; let machineMock: jest.Mocked<IMachineLearningRepository>;
let databaseMock: jest.Mocked<IDatabaseRepository>; let databaseMock: jest.Mocked<IDatabaseRepository>;
beforeEach(async () => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
searchMock = newSearchRepositoryMock(); searchMock = newSearchRepositoryMock();

View File

@ -45,7 +45,7 @@ describe(StorageTemplateService.name, () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
beforeEach(async () => { beforeEach(() => {
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
albumMock = newAlbumRepositoryMock(); albumMock = newAlbumRepositoryMock();

View File

@ -6,7 +6,7 @@ describe(StorageService.name, () => {
let sut: StorageService; let sut: StorageService;
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
beforeEach(async () => { beforeEach(() => {
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();
sut = new StorageService(storageMock); sut = new StorageService(storageMock);
}); });

View File

@ -148,7 +148,7 @@ describe(SystemConfigService.name, () => {
let communicationMock: jest.Mocked<ICommunicationRepository>; let communicationMock: jest.Mocked<ICommunicationRepository>;
let smartInfoMock: jest.Mocked<ISearchRepository>; let smartInfoMock: jest.Mocked<ISearchRepository>;
beforeEach(async () => { beforeEach(() => {
delete process.env.IMMICH_CONFIG_FILE; delete process.env.IMMICH_CONFIG_FILE;
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
communicationMock = newCommunicationRepositoryMock(); communicationMock = newCommunicationRepositoryMock();

View File

@ -118,7 +118,7 @@ export class SystemConfigService {
await this.core.refreshConfig(); await this.core.refreshConfig();
} }
private async setLogLevel({ logging }: SystemConfig) { private setLogLevel({ logging }: SystemConfig) {
const envLevel = this.getEnvLogLevel(); const envLevel = this.getEnvLogLevel();
const configLevel = logging.enabled ? logging.level : false; const configLevel = logging.enabled ? logging.level : false;
const level = envLevel ?? configLevel; const level = envLevel ?? configLevel;
@ -130,7 +130,7 @@ export class SystemConfigService {
return process.env.LOG_LEVEL as LogLevel; return process.env.LOG_LEVEL as LogLevel;
} }
private async validateConfig(newConfig: SystemConfig, oldConfig: SystemConfig) { private validateConfig(newConfig: SystemConfig, oldConfig: SystemConfig) {
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) {
throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.'); throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.');
} }

View File

@ -23,7 +23,7 @@ describe(TrashService.name, () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
beforeEach(async () => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
communicationMock = newCommunicationRepositoryMock(); communicationMock = newCommunicationRepositoryMock();

View File

@ -49,7 +49,7 @@ describe(UserService.name, () => {
let libraryMock: jest.Mocked<ILibraryRepository>; let libraryMock: jest.Mocked<ILibraryRepository>;
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
beforeEach(async () => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
cryptoRepositoryMock = newCryptoRepositoryMock(); cryptoRepositoryMock = newCryptoRepositoryMock();

View File

@ -15,7 +15,7 @@ import { routeToErrorMessage } from '../app.utils';
export class ErrorInterceptor implements NestInterceptor { export class ErrorInterceptor implements NestInterceptor {
private logger = new ImmichLogger(ErrorInterceptor.name); private logger = new ImmichLogger(ErrorInterceptor.name);
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> { intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
return next.handle().pipe( return next.handle().pipe(
catchError((error) => catchError((error) =>
throwError(() => { throwError(() => {

View File

@ -40,9 +40,9 @@ interface Callback<T> {
(error: null, result: T): void; (error: null, result: T): void;
} }
const callbackify = async <T>(target: (...arguments_: any[]) => T, callback: Callback<T>) => { const callbackify = <T>(target: (...arguments_: any[]) => T, callback: Callback<T>) => {
try { try {
return callback(null, await target()); return callback(null, target());
} catch (error: Error | any) { } catch (error: Error | any) {
return callback(error); return callback(error);
} }

View File

@ -544,7 +544,7 @@ export class AssetRepository implements IAssetRepository {
} }
async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> { async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> {
let builder = await this.repository let builder = this.repository
.createQueryBuilder('asset') .createQueryBuilder('asset')
.select(`COUNT(asset.id)`, 'count') .select(`COUNT(asset.id)`, 'count')
.addSelect(`asset.type`, 'type') .addSelect(`asset.type`, 'type')

View File

@ -166,7 +166,7 @@ export class LibraryRepository implements ILibraryRepository {
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
async getAssetIds(libraryId: string, withDeleted = false): Promise<string[]> { async getAssetIds(libraryId: string, withDeleted = false): Promise<string[]> {
let query = await this.repository let query = this.repository
.createQueryBuilder('library') .createQueryBuilder('library')
.innerJoinAndSelect('library.assets', 'assets') .innerJoinAndSelect('library.assets', 'assets')
.where('library.id = :id', { id: libraryId }) .where('library.id = :id', { id: libraryId })

View File

@ -1,4 +1,11 @@
import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoInfo } from '@app/domain'; import {
CropOptions,
IMediaRepository,
ResizeOptions,
TranscodeOptions,
VideoInfo,
handlePromiseError,
} from '@app/domain';
import { Colorspace } from '@app/infra/entities'; import { Colorspace } from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger'; import { ImmichLogger } from '@app/infra/logger';
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
@ -99,8 +106,8 @@ export class MediaRepository implements IMediaRepository {
.addOptions('-pass', '2') .addOptions('-pass', '2')
.addOptions('-passlogfile', output) .addOptions('-passlogfile', output)
.on('error', reject) .on('error', reject)
.on('end', () => fs.unlink(`${output}-0.log`)) .on('end', () => handlePromiseError(fs.unlink(`${output}-0.log`), this.logger))
.on('end', () => fs.rm(`${output}-0.log.mbtree`, { force: true })) .on('end', () => handlePromiseError(fs.rm(`${output}-0.log.mbtree`, { force: true }), this.logger))
.on('end', resolve) .on('end', resolve)
.run(); .run();
}) })

View File

@ -75,22 +75,22 @@ class JobMock implements IJobRepository {
async resume() {} async resume() {}
async empty() {} async empty() {}
async setConcurrency() {} async setConcurrency() {}
async getQueueStatus() { getQueueStatus() {
return null as any; return Promise.resolve(null) as any;
} }
async getJobCounts() { getJobCounts() {
return null as any; return Promise.resolve(null) as any;
} }
async pause() {} async pause() {}
async clear() { clear() {
return []; return Promise.resolve([]);
} }
async waitForQueueCompletion() {} async waitForQueueCompletion() {}
} }
class MediaMockRepository extends MediaRepository { class MediaMockRepository extends MediaRepository {
async generateThumbhash() { generateThumbhash() {
return Buffer.from('mock-thumbhash'); return Promise.resolve(Buffer.from('mock-thumbhash'));
} }
} }

View File

@ -3,7 +3,7 @@ import { WatchOptions } from 'chokidar';
interface MockWatcherOptions { interface MockWatcherOptions {
items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>;
close?: () => void; close?: () => Promise<void>;
} }
export const makeMockWatcher = export const makeMockWatcher =
@ -29,7 +29,12 @@ export const makeMockWatcher =
} }
} }
} }
return () => close?.();
if (close) {
return () => close();
}
return () => Promise.resolve();
}; };
export const newStorageRepositoryMock = (reset = true): jest.Mocked<IStorageRepository> => { export const newStorageRepositoryMock = (reset = true): jest.Mocked<IStorageRepository> => {