From 2190aa72a8ea0ee777a55371e14ed7941f25e452 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 4 Jun 2026 00:21:07 +0200 Subject: [PATCH] refactor(server): zod int validation (#28804) --- server/src/repositories/database.repository.ts | 18 +++++++++++++++--- server/src/repositories/search.repository.ts | 6 +++--- server/src/validation.ts | 5 ----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index df69e85d84..a4e58c52ec 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -24,7 +24,7 @@ import { DB } from 'src/schema'; import { immich_uuid_v7 } from 'src/schema/functions'; import { ExtensionVersion, VectorExtension } from 'src/types'; import { vectorIndexQuery } from 'src/utils/database'; -import { isValidInteger } from 'src/validation'; +import z from 'zod'; export let cachedVectorExtension: VectorExtension | undefined; export async function getVectorExtension(runner: Kysely): Promise { @@ -292,7 +292,13 @@ export class DatabaseRepository { `.execute(this.db); const dimSize = rows[0]?.dimsize; - if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { + if ( + !z + .int() + .min(1) + .max(2 ** 16) + .safeParse(dimSize).success + ) { this.logger.warn(`Could not retrieve dimension size of column '${column}' in table '${table}', assuming 512`); return 512; } @@ -300,7 +306,13 @@ export class DatabaseRepository { } async setDimensionSize(dimSize: number): Promise { - if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) { + if ( + !z + .int() + .min(1) + .max(2 ** 16) + .safeParse(dimSize).success + ) { throw new Error(`Invalid CLIP dimension size: ${dimSize}`); } diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 6f03c80ce1..da3f31555a 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -8,7 +8,7 @@ import { DB } from 'src/schema'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { anyUuid, searchAssetBuilder, withExifInner } from 'src/utils/database'; import { paginationHelper } from 'src/utils/pagination'; -import { isValidInteger } from 'src/validation'; +import z from 'zod'; export interface SearchAssetIdOptions { checksum?: Buffer; @@ -278,7 +278,7 @@ export class SearchRepository { ], }) searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) { - if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) { + if (!z.int().min(1).max(1000).safeParse(pagination.size).success) { throw new Error(`Invalid value for 'size': ${pagination.size}`); } @@ -313,7 +313,7 @@ export class SearchRepository { ], }) searchFaces({ userIds, embedding, numResults, maxDistance, hasPerson, minBirthDate }: FaceEmbeddingSearch) { - if (!isValidInteger(numResults, { min: 1, max: 1000 })) { + if (!z.int().min(1).max(1000).safeParse(numResults).success) { throw new Error(`Invalid value for 'numResults': ${numResults}`); } diff --git a/server/src/validation.ts b/server/src/validation.ts index 97d4b71964..6a57bc5582 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -125,11 +125,6 @@ const FilenameParamSchema = z.object({ export class FilenameParamDto extends createZodDto(FilenameParamSchema) {} -export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { - const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; - return Number.isInteger(value) && value >= min && value <= max; -}; - /** * Unified email validation * Converts email strings to lowercase and validates against HTML5 email regex