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 { 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');
}
}

View file

@ -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: {

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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

View file

@ -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<AssetIdsResponseDto[]> {

View file

@ -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();

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 { 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');
}
}

View file

@ -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) {

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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 = <T>(promise: Promise<T>, logger: LoggingReposi
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 {
title: string;
description: string;