Files
immich/server/src/dtos/search.dto.ts
T
Timon 013ea37a0d refactor!: change number to integer types (#27912)
* refactor!: change number to integer types

* fix oversight
2026-04-28 11:25:03 -04:00

229 lines
10 KiB
TypeScript

import { createZodDto } from 'nestjs-zod';
import { Place } from 'src/database';
import { HistoryBuilder } from 'src/decorators';
import { AlbumResponseSchema } from 'src/dtos/album.dto';
import { AssetResponseSchema } from 'src/dtos/asset-response.dto';
import { AssetOrder, AssetOrderSchema, AssetTypeSchema, AssetVisibilitySchema } from 'src/enum';
import { emptyStringToNull, isoDatetimeToDate, stringToBool } from 'src/validation';
import z from 'zod';
const BaseSearchSchema = z.object({
libraryId: z.uuidv4().nullish().describe('Library ID to filter by'),
type: AssetTypeSchema.optional(),
isEncoded: z.boolean().optional().describe('Filter by encoded status'),
isFavorite: z.boolean().optional().describe('Filter by favorite status'),
isMotion: z.boolean().optional().describe('Filter by motion photo status'),
isOffline: z.boolean().optional().describe('Filter by offline status'),
visibility: AssetVisibilitySchema.optional(),
createdBefore: isoDatetimeToDate.optional().describe('Filter by creation date (before)'),
createdAfter: isoDatetimeToDate.optional().describe('Filter by creation date (after)'),
updatedBefore: isoDatetimeToDate.optional().describe('Filter by update date (before)'),
updatedAfter: isoDatetimeToDate.optional().describe('Filter by update date (after)'),
trashedBefore: isoDatetimeToDate.optional().describe('Filter by trash date (before)'),
trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)'),
takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)'),
takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)'),
city: emptyStringToNull(z.string().nullable()).optional().describe('Filter by city name'),
state: emptyStringToNull(z.string().nullable()).optional().describe('Filter by state/province name'),
country: emptyStringToNull(z.string().nullable()).optional().describe('Filter by country name'),
make: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera make'),
model: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera model'),
lensModel: emptyStringToNull(z.string().nullable()).optional().describe('Filter by lens model'),
isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album'),
personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs'),
tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'),
albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'),
rating: z
.int()
.min(-1)
.max(5)
.nullish()
.describe('Filter by rating [1-5], or null for unrated')
.meta({
...new HistoryBuilder()
.added('v1')
.stable('v2')
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.')
.getExtensions(),
}),
ocr: z.string().optional().describe('Filter by OCR text content'),
});
const BaseSearchWithResultsSchema = BaseSearchSchema.extend({
withDeleted: z.boolean().optional().describe('Include deleted assets'),
withExif: z.boolean().optional().describe('Include EXIF data in response'),
size: z.int().min(1).max(1000).optional().describe('Number of results to return'),
});
const RandomSearchSchema = BaseSearchWithResultsSchema.extend({
withStacked: z.boolean().optional().describe('Include stacked assets'),
withPeople: z.boolean().optional().describe('Include people data in response'),
}).meta({ id: 'RandomSearchDto' });
const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({
minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'),
size: z.coerce.number().int().min(1).max(1000).optional().describe('Number of results to return'),
}).meta({ id: 'LargeAssetSearchDto' });
const MetadataSearchSchema = RandomSearchSchema.extend({
id: z.uuidv4().optional().describe('Filter by asset ID'),
description: z.string().trim().optional().describe('Filter by description text'),
checksum: z.string().optional().describe('Filter by file checksum'),
originalFileName: z.string().trim().optional().describe('Filter by original file name'),
originalPath: z.string().optional().describe('Filter by original file path'),
previewPath: z.string().optional().describe('Filter by preview file path'),
thumbnailPath: z.string().optional().describe('Filter by thumbnail file path'),
encodedVideoPath: z.string().optional().describe('Filter by encoded video file path'),
order: AssetOrderSchema.default(AssetOrder.Desc).optional().describe('Sort order'),
page: z.int().min(1).optional().describe('Page number'),
}).meta({ id: 'MetadataSearchDto' });
const StatisticsSearchSchema = BaseSearchSchema.extend({
description: z.string().trim().optional().describe('Filter by description text'),
}).meta({ id: 'StatisticsSearchDto' });
const SmartSearchSchema = BaseSearchWithResultsSchema.extend({
query: z.string().trim().optional().describe('Natural language search query'),
queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'),
language: z.string().optional().describe('Search language code'),
page: z.int().min(1).optional().describe('Page number'),
}).meta({ id: 'SmartSearchDto' });
const SearchPlacesSchema = z
.object({
name: z.string().describe('Place name to search for'),
})
.meta({ id: 'SearchPlacesDto' });
const SearchPeopleSchema = z
.object({
name: z.string().describe('Person name to search for'),
withHidden: stringToBool.optional().describe('Include hidden people'),
})
.meta({ id: 'SearchPeopleDto' });
const PlacesResponseSchema = z
.object({
name: z.string().describe('Place name'),
latitude: z.number().describe('Latitude coordinate'),
longitude: z.number().describe('Longitude coordinate'),
admin1name: z.string().optional().describe('Administrative level 1 name (state/province)'),
admin2name: z.string().optional().describe('Administrative level 2 name (county/district)'),
})
.meta({ id: 'PlacesResponseDto' });
export enum SearchSuggestionType {
COUNTRY = 'country',
STATE = 'state',
CITY = 'city',
CAMERA_MAKE = 'camera-make',
CAMERA_MODEL = 'camera-model',
CAMERA_LENS_MODEL = 'camera-lens-model',
}
const SearchSuggestionTypeSchema = z
.enum(SearchSuggestionType)
.describe('Suggestion type')
.meta({ id: 'SearchSuggestionType' });
const SearchSuggestionRequestSchema = z
.object({
type: SearchSuggestionTypeSchema,
country: z.string().optional().describe('Filter by country'),
state: z.string().optional().describe('Filter by state/province'),
make: z.string().optional().describe('Filter by camera make'),
model: z.string().optional().describe('Filter by camera model'),
lensModel: z.string().optional().describe('Filter by lens model'),
includeNull: stringToBool
.optional()
.describe('Include null values in suggestions')
.meta(new HistoryBuilder().added('v1.111.0').stable('v2').getExtensions()),
})
.meta({ id: 'SearchSuggestionRequestDto' });
export class RandomSearchDto extends createZodDto(RandomSearchSchema) {}
export class LargeAssetSearchDto extends createZodDto(LargeAssetSearchSchema) {}
export class MetadataSearchDto extends createZodDto(MetadataSearchSchema) {}
export class StatisticsSearchDto extends createZodDto(StatisticsSearchSchema) {}
export class SmartSearchDto extends createZodDto(SmartSearchSchema) {}
export class SearchPlacesDto extends createZodDto(SearchPlacesSchema) {}
export class SearchPeopleDto extends createZodDto(SearchPeopleSchema) {}
export class PlacesResponseDto extends createZodDto(PlacesResponseSchema) {}
export class SearchSuggestionRequestDto extends createZodDto(SearchSuggestionRequestSchema) {}
export function mapPlaces(place: Place): PlacesResponseDto {
return {
name: place.name,
latitude: place.latitude,
longitude: place.longitude,
admin1name: place.admin1Name ?? undefined,
admin2name: place.admin2Name ?? undefined,
};
}
const SearchFacetCountResponseSchema = z
.object({
count: z.int().min(0).describe('Number of assets with this facet value'),
value: z.string().describe('Facet value'),
})
.meta({ id: 'SearchFacetCountResponseDto' });
const SearchFacetResponseSchema = z
.object({
fieldName: z.string().describe('Facet field name'),
counts: z.array(SearchFacetCountResponseSchema),
})
.meta({ id: 'SearchFacetResponseDto' });
const SearchAlbumResponseSchema = z
.object({
total: z.int().min(0).describe('Total number of matching albums'),
count: z.int().min(0).describe('Number of albums in this page'),
items: z.array(AlbumResponseSchema),
facets: z.array(SearchFacetResponseSchema),
})
.meta({ id: 'SearchAlbumResponseDto' });
const SearchAssetResponseSchema = z
.object({
total: z.int().min(0).describe('Total number of matching assets'),
count: z.int().min(0).describe('Number of assets in this page'),
items: z.array(AssetResponseSchema),
facets: z.array(SearchFacetResponseSchema),
nextPage: z.string().nullable().describe('Next page token'),
})
.meta({ id: 'SearchAssetResponseDto' });
const SearchResponseSchema = z
.object({
albums: SearchAlbumResponseSchema,
assets: SearchAssetResponseSchema,
})
.meta({ id: 'SearchResponseDto' });
export class SearchResponseDto extends createZodDto(SearchResponseSchema) {}
const SearchStatisticsResponseSchema = z
.object({
total: z.int().describe('Total number of matching assets'),
})
.meta({ id: 'SearchStatisticsResponseDto' });
export class SearchStatisticsResponseDto extends createZodDto(SearchStatisticsResponseSchema) {}
const SearchExploreItemSchema = z
.object({
value: z.string().describe('Explore value'),
data: AssetResponseSchema,
})
.meta({ id: 'SearchExploreItem' });
const SearchExploreResponseSchema = z
.object({
fieldName: z.string().describe('Explore field name'),
items: z.array(SearchExploreItemSchema),
})
.meta({ id: 'SearchExploreResponseDto' });
export class SearchExploreResponseDto extends createZodDto(SearchExploreResponseSchema) {}