From 927b4d0ea287470e669bee6004e302a6e2bf1b6c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 11 Aug 2026 09:27:35 -0400 Subject: [PATCH] refactor: find or fail (#30697) --- server/src/services/album.service.ts | 9 +++------ server/src/services/asset.service.ts | 9 +++------ server/src/services/library.service.ts | 10 +++------- server/src/services/memory.service.ts | 11 ++++------- server/src/services/person.service.ts | 10 +++------- server/src/services/shared-link.service.ts | 10 +++------- server/src/services/stack.service.spec.ts | 6 ++++-- server/src/services/stack.service.ts | 10 +++------- server/src/services/tag.service.ts | 9 +++------ server/src/services/user-admin.service.ts | 9 +++------ server/src/services/user.service.ts | 9 +++------ server/src/services/workflow.service.ts | 9 +++------ server/src/utils/misc.ts | 11 ++++++++++- 13 files changed, 48 insertions(+), 74 deletions(-) diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 071c5d5aec..a94049bbd2 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -19,6 +19,7 @@ import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.reposi import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { asDateTimeString } from 'src/utils/date'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences } from 'src/utils/preferences'; @Injectable() @@ -351,11 +352,7 @@ export class AlbumService extends BaseService { await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } - private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { - const album = await this.albumRepository.getById(id, options, authUserId); - if (!album) { - throw new BadRequestException('Album not found'); - } - return album; + private findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { + return findOrFail(() => this.albumRepository.getById(id, options, authUserId), 'Album'); } } diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 1edccf9483..72b5ad997b 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -46,6 +46,7 @@ import { } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; import { extractTimeZone } from 'src/utils/date'; +import { findOrFail } from 'src/utils/misc'; import { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() @@ -496,12 +497,8 @@ export class AssetService extends BaseService { await this.jobRepository.queueAll(jobs); } - private async findOrFail(id: string) { - const asset = await this.assetRepository.getById(id); - if (!asset) { - throw new BadRequestException('Asset not found'); - } - return asset; + private findOrFail(id: string) { + return findOrFail(() => this.assetRepository.getById(id), 'Asset'); } private async updateExif(dto: { diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index f13a440f84..56e5fa6be8 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -34,7 +34,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; import { mimeTypes } from 'src/utils/mime-types'; -import { handlePromiseError } from 'src/utils/misc'; +import { findOrFail, handlePromiseError } from 'src/utils/misc'; @Injectable() export class LibraryService extends BaseService { @@ -793,11 +793,7 @@ export class LibraryService extends BaseService { return JobStatus.Success; } - private async findOrFail(id: string) { - const library = await this.libraryRepository.get(id); - if (!library) { - throw new BadRequestException('Library not found'); - } - return library; + private findOrFail(id: string) { + return findOrFail(() => this.libraryRepository.get(id), 'Library'); } } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 41510056ff..1175a11af4 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { Memory } from 'src/database'; 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 { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { findOrFail } from 'src/utils/misc'; const DAYS = 3; @@ -162,11 +163,7 @@ export class MemoryService extends BaseService { return results; } - private async findOrFail(id: string) { - const memory = await this.memoryRepository.get(id); - if (!memory) { - throw new BadRequestException('Memory not found'); - } - return memory; + private findOrFail(id: string) { + return findOrFail(() => this.memoryRepository.get(id), 'Memory'); } } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 8e0cd2ff01..ff381ce736 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -43,7 +43,7 @@ import { JobItem, JobOf } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; 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'; @Injectable() @@ -614,12 +614,8 @@ export class PersonService extends BaseService { return results; } - private async findOrFail(id: string) { - const person = await this.personRepository.getById(id); - if (!person) { - throw new BadRequestException('Person not found'); - } - return person; + private findOrFail(id: string) { + return findOrFail(() => this.personRepository.getById(id), 'Person'); } // TODO return a asset face response diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 41b1d5403c..31ca32f552 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -13,7 +13,7 @@ import { } from 'src/dtos/shared-link.dto'; import { Permission, SharedLinkType } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; +import { findOrFail, getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { @@ -143,12 +143,8 @@ export class SharedLinkService extends BaseService { } // TODO: replace `userId` with permissions and access control checks - private async findOrFail(userId: string, id: string) { - const sharedLink = await this.sharedLinkRepository.get(userId, id); - if (!sharedLink) { - throw new BadRequestException('Shared link not found'); - } - return sharedLink; + private findOrFail(userId: string, id: string) { + return findOrFail(() => this.sharedLinkRepository.get(userId, id), 'Shared link'); } async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index d47d634f4f..933c7f39c5 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -85,8 +85,9 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { 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.stack.getById).toHaveBeenCalledWith('stack-id'); @@ -124,8 +125,9 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { 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.update).not.toHaveBeenCalled(); diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index 2dc9f1547e..9dc47a97bf 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; import { UUIDAssetIDParamDto } from 'src/validation'; @Injectable() @@ -77,12 +78,7 @@ export class StackService extends BaseService { await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id }); } - private async findOrFail(id: string) { - const stack = await this.stackRepository.getById(id); - if (!stack) { - throw new Error('Asset stack not found'); - } - - return stack; + private findOrFail(id: string) { + return findOrFail(() => this.stackRepository.getById(id), 'Asset stack'); } } diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 1c7195e4c3..3cd442b6a7 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -17,6 +17,7 @@ import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; +import { findOrFail } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; @Injectable() @@ -146,12 +147,8 @@ export class TagService extends BaseService { return JobStatus.Success; } - private async findOrFail(id: string) { - const tag = await this.tagRepository.get(id); - if (!tag) { - throw new BadRequestException('Tag not found'); - } - return tag; + private findOrFail(id: string) { + return findOrFail(() => this.tagRepository.get(id), 'Tag'); } private async updateTags(assetId: string) { diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 15ea8c0598..f80a810616 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -17,6 +17,7 @@ import { JobName, UserMetadataKey, UserStatus } from 'src/enum'; import { UserFindOptions } from 'src/repositories/user.repository'; import { BaseService } from 'src/services/base.service'; import { getCalendarHeatmap } from 'src/services/shared/user-methods'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() @@ -158,11 +159,7 @@ export class UserAdminService extends BaseService { return mapPreferences(newPreferences); } - private async findOrFail(id: string, options: UserFindOptions) { - const user = await this.userRepository.get(id, options); - if (!user) { - throw new BadRequestException('User not found'); - } - return user; + private findOrFail(id: string, options: UserFindOptions) { + return findOrFail(() => this.userRepository.get(id, options), 'User'); } } diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index c7a5bc4e02..1630fd9dd9 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -20,6 +20,7 @@ import { getCalendarHeatmap } from 'src/services/shared/user-methods'; import { JobOf, UserMetadataItem } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; 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); } - private async findOrFail(id: string, options: UserFindOptions) { - const user = await this.userRepository.get(id, options); - if (!user) { - throw new BadRequestException('User not found'); - } - return user; + private findOrFail(id: string, options: UserFindOptions) { + return findOrFail(() => this.userRepository.get(id, options), 'User'); } } diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 850b4ac086..1d4ecb89a0 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -14,6 +14,7 @@ import { import { Permission } from 'src/enum'; import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository'; import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow'; @Injectable() @@ -104,11 +105,7 @@ export class WorkflowService extends BaseService { return results; } - private async findOrFail(id: string) { - const workflow = await this.workflowRepository.get(id); - if (!workflow) { - throw new BadRequestException('Workflow not found'); - } - return workflow; + private findOrFail(id: string) { + return findOrFail(() => this.workflowRepository.get(id), 'Workflow'); } } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 0e7a9e6cdc..4db4140c1b 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -1,4 +1,4 @@ -import { INestApplication } from '@nestjs/common'; +import { BadRequestException, INestApplication } from '@nestjs/common'; import { ApiBodyOptions, DocumentBuilder, @@ -111,6 +111,15 @@ export const handlePromiseError = (promise: Promise, logger: LoggingReposi promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); }; +export const findOrFail = async (find: () => Promise, entity: string): Promise> => { + const value = await find(); + if (!value) { + throw new BadRequestException(`${entity} not found`); + } + + return value; +}; + export interface OpenGraphTags { title: string; description: string;