refactor: find or fail (#30697)

This commit is contained in:
Jason Rasmussen 2026-08-11 09:27:35 -04:00 committed by GitHub
parent 9773d72428
commit 927b4d0ea2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 48 additions and 74 deletions

View file

@ -19,6 +19,7 @@ import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.reposi
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
import { asDateTimeString } from 'src/utils/date'; import { asDateTimeString } from 'src/utils/date';
import { findOrFail } from 'src/utils/misc';
import { getPreferences } from 'src/utils/preferences'; import { getPreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
@ -351,11 +352,7 @@ export class AlbumService extends BaseService {
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
} }
private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { private findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) {
const album = await this.albumRepository.getById(id, options, authUserId); return findOrFail(() => this.albumRepository.getById(id, options, authUserId), 'Album');
if (!album) {
throw new BadRequestException('Album not found');
}
return album;
} }
} }

View file

@ -46,6 +46,7 @@ import {
} from 'src/utils/asset.util'; } from 'src/utils/asset.util';
import { updateLockedColumns } from 'src/utils/database'; import { updateLockedColumns } from 'src/utils/database';
import { extractTimeZone } from 'src/utils/date'; import { extractTimeZone } from 'src/utils/date';
import { findOrFail } from 'src/utils/misc';
import { transformOcrBoundingBox } from 'src/utils/transform'; import { transformOcrBoundingBox } from 'src/utils/transform';
@Injectable() @Injectable()
@ -496,12 +497,8 @@ export class AssetService extends BaseService {
await this.jobRepository.queueAll(jobs); await this.jobRepository.queueAll(jobs);
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const asset = await this.assetRepository.getById(id); return findOrFail(() => this.assetRepository.getById(id), 'Asset');
if (!asset) {
throw new BadRequestException('Asset not found');
}
return asset;
} }
private async updateExif(dto: { private async updateExif(dto: {

View file

@ -34,7 +34,7 @@ import { AssetTable } from 'src/schema/tables/asset.table';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { JobOf } from 'src/types'; import { JobOf } from 'src/types';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { handlePromiseError } from 'src/utils/misc'; import { findOrFail, handlePromiseError } from 'src/utils/misc';
@Injectable() @Injectable()
export class LibraryService extends BaseService { export class LibraryService extends BaseService {
@ -793,11 +793,7 @@ export class LibraryService extends BaseService {
return JobStatus.Success; return JobStatus.Success;
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const library = await this.libraryRepository.get(id); return findOrFail(() => this.libraryRepository.get(id), 'Library');
if (!library) {
throw new BadRequestException('Library not found');
}
return library;
} }
} }

View file

@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Memory } from 'src/database'; import { Memory } from 'src/database';
import { OnJob } from 'src/decorators'; import { OnJob } from 'src/decorators';
@ -8,6 +8,7 @@ import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, m
import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
import { findOrFail } from 'src/utils/misc';
const DAYS = 3; const DAYS = 3;
@ -162,11 +163,7 @@ export class MemoryService extends BaseService {
return results; return results;
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const memory = await this.memoryRepository.get(id); return findOrFail(() => this.memoryRepository.get(id), 'Memory');
if (!memory) {
throw new BadRequestException('Memory not found');
}
return memory;
} }
} }

View file

@ -43,7 +43,7 @@ import { JobItem, JobOf } from 'src/types';
import { getDimensions } from 'src/utils/asset.util'; import { getDimensions } from 'src/utils/asset.util';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { isFacialRecognitionEnabled } from 'src/utils/misc'; import { findOrFail, isFacialRecognitionEnabled } from 'src/utils/misc';
import { Point, transformPoints } from 'src/utils/transform'; import { Point, transformPoints } from 'src/utils/transform';
@Injectable() @Injectable()
@ -614,12 +614,8 @@ export class PersonService extends BaseService {
return results; return results;
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const person = await this.personRepository.getById(id); return findOrFail(() => this.personRepository.getById(id), 'Person');
if (!person) {
throw new BadRequestException('Person not found');
}
return person;
} }
// TODO return a asset face response // TODO return a asset face response

View file

@ -13,7 +13,7 @@ import {
} from 'src/dtos/shared-link.dto'; } from 'src/dtos/shared-link.dto';
import { Permission, SharedLinkType } from 'src/enum'; import { Permission, SharedLinkType } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; import { findOrFail, getExternalDomain, OpenGraphTags } from 'src/utils/misc';
@Injectable() @Injectable()
export class SharedLinkService extends BaseService { export class SharedLinkService extends BaseService {
@ -143,12 +143,8 @@ export class SharedLinkService extends BaseService {
} }
// TODO: replace `userId` with permissions and access control checks // TODO: replace `userId` with permissions and access control checks
private async findOrFail(userId: string, id: string) { private findOrFail(userId: string, id: string) {
const sharedLink = await this.sharedLinkRepository.get(userId, id); return findOrFail(() => this.sharedLinkRepository.get(userId, id), 'Shared link');
if (!sharedLink) {
throw new BadRequestException('Shared link not found');
}
return sharedLink;
} }
async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {

View file

@ -85,8 +85,9 @@ describe(StackService.name, () => {
it('should fail if stack could not be found', async () => { it('should fail if stack could not be found', async () => {
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
mocks.stack.getById.mockResolvedValue(void 0);
await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(Error); await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled();
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
@ -124,8 +125,9 @@ describe(StackService.name, () => {
it('should fail if stack could not be found', async () => { it('should fail if stack could not be found', async () => {
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
mocks.stack.getById.mockResolvedValue(void 0);
await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(Error); await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
expect(mocks.stack.update).not.toHaveBeenCalled(); expect(mocks.stack.update).not.toHaveBeenCalled();

View file

@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { findOrFail } from 'src/utils/misc';
import { UUIDAssetIDParamDto } from 'src/validation'; import { UUIDAssetIDParamDto } from 'src/validation';
@Injectable() @Injectable()
@ -77,12 +78,7 @@ export class StackService extends BaseService {
await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id }); await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id });
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const stack = await this.stackRepository.getById(id); return findOrFail(() => this.stackRepository.getById(id), 'Asset stack');
if (!stack) {
throw new Error('Asset stack not found');
}
return stack;
} }
} }

View file

@ -17,6 +17,7 @@ import { TagAssetTable } from 'src/schema/tables/tag-asset.table';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
import { updateLockedColumns } from 'src/utils/database'; import { updateLockedColumns } from 'src/utils/database';
import { findOrFail } from 'src/utils/misc';
import { upsertTags } from 'src/utils/tag'; import { upsertTags } from 'src/utils/tag';
@Injectable() @Injectable()
@ -146,12 +147,8 @@ export class TagService extends BaseService {
return JobStatus.Success; return JobStatus.Success;
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const tag = await this.tagRepository.get(id); return findOrFail(() => this.tagRepository.get(id), 'Tag');
if (!tag) {
throw new BadRequestException('Tag not found');
}
return tag;
} }
private async updateTags(assetId: string) { private async updateTags(assetId: string) {

View file

@ -17,6 +17,7 @@ import { JobName, UserMetadataKey, UserStatus } from 'src/enum';
import { UserFindOptions } from 'src/repositories/user.repository'; import { UserFindOptions } from 'src/repositories/user.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getCalendarHeatmap } from 'src/services/shared/user-methods'; import { getCalendarHeatmap } from 'src/services/shared/user-methods';
import { findOrFail } from 'src/utils/misc';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
@ -158,11 +159,7 @@ export class UserAdminService extends BaseService {
return mapPreferences(newPreferences); return mapPreferences(newPreferences);
} }
private async findOrFail(id: string, options: UserFindOptions) { private findOrFail(id: string, options: UserFindOptions) {
const user = await this.userRepository.get(id, options); return findOrFail(() => this.userRepository.get(id, options), 'User');
if (!user) {
throw new BadRequestException('User not found');
}
return user;
} }
} }

View file

@ -20,6 +20,7 @@ import { getCalendarHeatmap } from 'src/services/shared/user-methods';
import { JobOf, UserMetadataItem } from 'src/types'; import { JobOf, UserMetadataItem } from 'src/types';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { findOrFail } from 'src/utils/misc';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
import { generateProfileImage } from 'src/utils/profile-image'; import { generateProfileImage } from 'src/utils/profile-image';
@ -302,11 +303,7 @@ export class UserService extends BaseService {
return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt); return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt);
} }
private async findOrFail(id: string, options: UserFindOptions) { private findOrFail(id: string, options: UserFindOptions) {
const user = await this.userRepository.get(id, options); return findOrFail(() => this.userRepository.get(id, options), 'User');
if (!user) {
throw new BadRequestException('User not found');
}
return user;
} }
} }

View file

@ -14,6 +14,7 @@ import {
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository'; import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { findOrFail } from 'src/utils/misc';
import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow'; import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow';
@Injectable() @Injectable()
@ -104,11 +105,7 @@ export class WorkflowService extends BaseService {
return results; return results;
} }
private async findOrFail(id: string) { private findOrFail(id: string) {
const workflow = await this.workflowRepository.get(id); return findOrFail(() => this.workflowRepository.get(id), 'Workflow');
if (!workflow) {
throw new BadRequestException('Workflow not found');
}
return workflow;
} }
} }

View file

@ -1,4 +1,4 @@
import { INestApplication } from '@nestjs/common'; import { BadRequestException, INestApplication } from '@nestjs/common';
import { import {
ApiBodyOptions, ApiBodyOptions,
DocumentBuilder, DocumentBuilder,
@ -111,6 +111,15 @@ export const handlePromiseError = <T>(promise: Promise<T>, logger: LoggingReposi
promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
}; };
export const findOrFail = async <T>(find: () => Promise<T>, entity: string): Promise<NonNullable<T>> => {
const value = await find();
if (!value) {
throw new BadRequestException(`${entity} not found`);
}
return value;
};
export interface OpenGraphTags { export interface OpenGraphTags {
title: string; title: string;
description: string; description: string;