mirror of
https://github.com/immich-app/immich.git
synced 2026-07-30 23:50:36 -07:00
483 lines
19 KiB
TypeScript
483 lines
19 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,
|
|
SearchOrderField,
|
|
SearchOrderFieldSchema,
|
|
} from 'src/enum';
|
|
import { isoDatetimeToDate, nonEmptyPartial, stringToBool } from 'src/validation';
|
|
import z from 'zod';
|
|
|
|
const ADDED_V3_1 = new HistoryBuilder().added('v3.1.0').getExtensions();
|
|
|
|
// fields deprecated in favor of the structured filter tree
|
|
const DEPRECATED_FLAT_FIELD = {
|
|
...new HistoryBuilder().added('v1').stable('v2').deprecated('v3.1.0').getExtensions(),
|
|
deprecated: true,
|
|
};
|
|
|
|
const BaseSearchSchema = z.object({
|
|
libraryId: z.uuidv4().nullish().describe('Library ID to filter by').meta(DEPRECATED_FLAT_FIELD),
|
|
type: AssetTypeSchema.optional().meta(DEPRECATED_FLAT_FIELD),
|
|
isEncoded: z.boolean().optional().describe('Filter by encoded status').meta(DEPRECATED_FLAT_FIELD),
|
|
isFavorite: z.boolean().optional().describe('Filter by favorite status').meta(DEPRECATED_FLAT_FIELD),
|
|
isMotion: z.boolean().optional().describe('Filter by motion photo status').meta(DEPRECATED_FLAT_FIELD),
|
|
isOffline: z.boolean().optional().describe('Filter by offline status').meta(DEPRECATED_FLAT_FIELD),
|
|
visibility: AssetVisibilitySchema.optional().meta(DEPRECATED_FLAT_FIELD),
|
|
createdBefore: isoDatetimeToDate.optional().describe('Filter by creation date (before)').meta(DEPRECATED_FLAT_FIELD),
|
|
createdAfter: isoDatetimeToDate.optional().describe('Filter by creation date (after)').meta(DEPRECATED_FLAT_FIELD),
|
|
updatedBefore: isoDatetimeToDate.optional().describe('Filter by update date (before)').meta(DEPRECATED_FLAT_FIELD),
|
|
updatedAfter: isoDatetimeToDate.optional().describe('Filter by update date (after)').meta(DEPRECATED_FLAT_FIELD),
|
|
trashedBefore: isoDatetimeToDate.optional().describe('Filter by trash date (before)').meta(DEPRECATED_FLAT_FIELD),
|
|
trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)').meta(DEPRECATED_FLAT_FIELD),
|
|
takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)').meta(DEPRECATED_FLAT_FIELD),
|
|
takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)').meta(DEPRECATED_FLAT_FIELD),
|
|
city: z.string().nullable().optional().describe('Filter by city name').meta(DEPRECATED_FLAT_FIELD),
|
|
state: z.string().nullable().optional().describe('Filter by state/province name').meta(DEPRECATED_FLAT_FIELD),
|
|
country: z.string().nullable().optional().describe('Filter by country name').meta(DEPRECATED_FLAT_FIELD),
|
|
make: z.string().nullable().optional().describe('Filter by camera make').meta(DEPRECATED_FLAT_FIELD),
|
|
model: z.string().nullable().optional().describe('Filter by camera model').meta(DEPRECATED_FLAT_FIELD),
|
|
lensModel: z.string().nullable().optional().describe('Filter by lens model').meta(DEPRECATED_FLAT_FIELD),
|
|
isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album').meta(DEPRECATED_FLAT_FIELD),
|
|
personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs').meta(DEPRECATED_FLAT_FIELD),
|
|
tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs').meta(DEPRECATED_FLAT_FIELD),
|
|
albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs').meta(DEPRECATED_FLAT_FIELD),
|
|
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.')
|
|
.updated('v3', 'Using -1 as a rating is no longer valid.')
|
|
.deprecated('v3.1.0')
|
|
.getExtensions(),
|
|
deprecated: true,
|
|
}),
|
|
ocr: z.string().optional().describe('Filter by OCR text content').meta(DEPRECATED_FLAT_FIELD),
|
|
});
|
|
|
|
const BaseSearchWithResultsSchema = BaseSearchSchema.extend({
|
|
withDeleted: z.boolean().optional().describe('Include deleted assets').meta(DEPRECATED_FLAT_FIELD),
|
|
withExif: z.boolean().optional().describe('Include EXIF data in response'),
|
|
size: z.int().min(1).max(1000).default(250).describe('Number of results to return'),
|
|
});
|
|
|
|
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).default(250).describe('Number of results to return'),
|
|
}).meta({ id: 'LargeAssetSearchDto' });
|
|
|
|
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' });
|
|
|
|
const IdFilterSchema = nonEmptyPartial({
|
|
eq: z.uuidv4(),
|
|
ne: z.uuidv4(),
|
|
}).meta({ id: 'IdFilter' });
|
|
|
|
const IdFilterNullableSchema = nonEmptyPartial({
|
|
eq: z.uuidv4().nullable(),
|
|
ne: z.uuidv4().nullable(),
|
|
}).meta({ id: 'IdFilterNullable' });
|
|
|
|
const IdsFilterSchema = nonEmptyPartial({
|
|
any: z.array(z.uuidv4()).min(1),
|
|
all: z.array(z.uuidv4()).min(1),
|
|
none: z.array(z.uuidv4()).min(1),
|
|
}).meta({ id: 'IdsFilter' });
|
|
|
|
const stringListShape = {
|
|
in: z.array(z.string()).min(1),
|
|
notIn: z.array(z.string()).min(1),
|
|
};
|
|
|
|
const StringFilterSchema = nonEmptyPartial({
|
|
eq: z.string(),
|
|
ne: z.string(),
|
|
...stringListShape,
|
|
}).meta({ id: 'StringFilter' });
|
|
|
|
const stringNullableShape = {
|
|
eq: z.string().nullable(),
|
|
ne: z.string().nullable(),
|
|
...stringListShape,
|
|
};
|
|
|
|
const StringFilterNullableSchema = nonEmptyPartial(stringNullableShape).meta({ id: 'StringFilterNullable' });
|
|
|
|
const StringPatternFilterSchema = nonEmptyPartial({
|
|
...stringNullableShape,
|
|
like: z.string().min(1),
|
|
notLike: z.string().min(1),
|
|
startsWith: z.string().min(1),
|
|
endsWith: z.string().min(1),
|
|
}).meta({ id: 'StringPatternFilter' });
|
|
|
|
const numberRangeShape = {
|
|
lt: z.number(),
|
|
lte: z.number(),
|
|
gt: z.number(),
|
|
gte: z.number(),
|
|
in: z.array(z.number()).min(1),
|
|
notIn: z.array(z.number()).min(1),
|
|
};
|
|
|
|
const NumberFilterSchema = nonEmptyPartial({
|
|
eq: z.number(),
|
|
ne: z.number(),
|
|
...numberRangeShape,
|
|
}).meta({ id: 'NumberFilter' });
|
|
|
|
const NumberFilterNullableSchema = nonEmptyPartial({
|
|
eq: z.number().nullable(),
|
|
ne: z.number().nullable(),
|
|
...numberRangeShape,
|
|
}).meta({ id: 'NumberFilterNullable' });
|
|
|
|
const dateRangeShape = {
|
|
gt: isoDatetimeToDate,
|
|
gte: isoDatetimeToDate,
|
|
lt: isoDatetimeToDate,
|
|
lte: isoDatetimeToDate,
|
|
};
|
|
|
|
const DateFilterSchema = nonEmptyPartial({
|
|
eq: isoDatetimeToDate,
|
|
ne: isoDatetimeToDate,
|
|
...dateRangeShape,
|
|
}).meta({ id: 'DateFilter' });
|
|
|
|
const DateFilterNullableSchema = nonEmptyPartial({
|
|
eq: isoDatetimeToDate.nullable(),
|
|
ne: isoDatetimeToDate.nullable(),
|
|
...dateRangeShape,
|
|
}).meta({ id: 'DateFilterNullable' });
|
|
|
|
const BoolFilterSchema = z.object({ eq: z.boolean() }).meta({ id: 'BoolFilter' });
|
|
|
|
const enumFilterSchema = <T extends z.core.util.EnumLike>(values: z.ZodEnum<T>, id: string) =>
|
|
nonEmptyPartial({
|
|
eq: values,
|
|
ne: values,
|
|
in: z.array(values).min(1),
|
|
notIn: z.array(values).min(1),
|
|
}).meta({ id });
|
|
|
|
const EnumFilterAssetTypeSchema = enumFilterSchema(AssetTypeSchema, 'EnumFilterAssetType');
|
|
const EnumFilterAssetVisibilitySchema = enumFilterSchema(AssetVisibilitySchema, 'EnumFilterAssetVisibility');
|
|
|
|
const StringSimilarityFilterSchema = z
|
|
.object({
|
|
matches: z.string().min(1),
|
|
})
|
|
.meta({ id: 'StringSimilarityFilter' });
|
|
|
|
export const DEFAULT_SEARCH_ORDER = {
|
|
field: SearchOrderField.FileCreatedAt,
|
|
direction: AssetOrder.Desc,
|
|
};
|
|
|
|
export const SearchOrderSchema = z
|
|
.object({
|
|
field: SearchOrderFieldSchema.default(DEFAULT_SEARCH_ORDER.field),
|
|
direction: AssetOrderSchema.default(DEFAULT_SEARCH_ORDER.direction),
|
|
})
|
|
.meta({ id: 'SearchOrder' });
|
|
|
|
const searchFilterBranchShape = {
|
|
id: IdFilterSchema,
|
|
libraryId: IdFilterNullableSchema,
|
|
type: EnumFilterAssetTypeSchema,
|
|
visibility: EnumFilterAssetVisibilitySchema,
|
|
isFavorite: BoolFilterSchema,
|
|
isMotion: BoolFilterSchema,
|
|
isOffline: BoolFilterSchema,
|
|
isEncoded: BoolFilterSchema,
|
|
hasAlbums: BoolFilterSchema,
|
|
hasPeople: BoolFilterSchema,
|
|
hasTags: BoolFilterSchema,
|
|
city: StringFilterNullableSchema,
|
|
state: StringFilterNullableSchema,
|
|
country: StringFilterNullableSchema,
|
|
make: StringFilterNullableSchema,
|
|
model: StringFilterNullableSchema,
|
|
lensModel: StringFilterNullableSchema,
|
|
description: StringPatternFilterSchema,
|
|
originalFileName: StringPatternFilterSchema,
|
|
originalPath: StringPatternFilterSchema,
|
|
ocr: StringSimilarityFilterSchema,
|
|
rating: NumberFilterNullableSchema,
|
|
fileSizeInBytes: NumberFilterSchema,
|
|
takenAt: DateFilterSchema,
|
|
createdAt: DateFilterSchema,
|
|
updatedAt: DateFilterSchema,
|
|
trashedAt: DateFilterNullableSchema,
|
|
personIds: IdsFilterSchema,
|
|
tagIds: IdsFilterSchema,
|
|
albumIds: IdsFilterSchema,
|
|
checksum: StringFilterSchema,
|
|
encodedVideoPath: StringFilterSchema,
|
|
};
|
|
|
|
const SearchFilterBranchSchema = z
|
|
.strictObject(searchFilterBranchShape)
|
|
.partial()
|
|
.refine((branch) => Object.values(branch).some((value) => value !== undefined), {
|
|
message: 'At least one filter condition is required',
|
|
})
|
|
.meta({ id: 'SearchFilterBranch' });
|
|
|
|
export const SearchFilterSchema = z
|
|
.strictObject(searchFilterBranchShape)
|
|
.partial()
|
|
.extend({
|
|
or: z.array(SearchFilterBranchSchema).min(1).optional(),
|
|
})
|
|
.meta({ id: 'SearchFilter' });
|
|
|
|
export type IdFilter = z.infer<typeof IdFilterSchema>;
|
|
export type IdFilterNullable = z.infer<typeof IdFilterNullableSchema>;
|
|
export type IdsFilter = z.infer<typeof IdsFilterSchema>;
|
|
export type StringFilter = z.infer<typeof StringFilterSchema>;
|
|
export type StringFilterNullable = z.infer<typeof StringFilterNullableSchema>;
|
|
export type StringPatternFilter = z.infer<typeof StringPatternFilterSchema>;
|
|
export type NumberFilter = z.infer<typeof NumberFilterSchema>;
|
|
export type NumberFilterNullable = z.infer<typeof NumberFilterNullableSchema>;
|
|
export type DateFilter = z.infer<typeof DateFilterSchema>;
|
|
export type DateFilterNullable = z.infer<typeof DateFilterNullableSchema>;
|
|
export type SearchOrder = z.infer<typeof SearchOrderSchema>;
|
|
export type SearchFilter = z.infer<typeof SearchFilterSchema>;
|
|
export type SearchFilterBranch = z.infer<typeof SearchFilterBranchSchema>;
|
|
|
|
const NEW_SHAPE_FIELDS = ['filter', 'orderBy', 'cursor'] as const;
|
|
|
|
export const isNewShapeRequest = (dto: Partial<Record<(typeof NEW_SHAPE_FIELDS)[number], unknown>>): boolean =>
|
|
NEW_SHAPE_FIELDS.some((field) => dto[field] !== undefined);
|
|
|
|
/**
|
|
* The structured shape and the deprecated flat search fields are mutually exclusive
|
|
* TODO(v4): remove together with the deprecated flat fields.
|
|
*/
|
|
const withShapeExclusivity = <T extends z.ZodObject<z.ZodRawShape>>(schema: T) => {
|
|
const deprecatedFields = Object.keys(schema.shape).filter(
|
|
(field) => (schema.shape[field] as z.ZodType).meta()?.deprecated,
|
|
);
|
|
|
|
return schema.superRefine((dto, ctx) => {
|
|
const values = dto as Record<string, unknown>;
|
|
const newShapeFields = NEW_SHAPE_FIELDS.filter((field) => values[field] !== undefined);
|
|
if (newShapeFields.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const field of deprecatedFields) {
|
|
if (values[field] === undefined) {
|
|
continue;
|
|
}
|
|
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: [field],
|
|
message: `Deprecated field ${field} cannot be combined with ${newShapeFields.join('/')}`,
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const filterField = SearchFilterSchema.optional().meta(ADDED_V3_1);
|
|
const cursorField = z.string().min(1).optional().describe('Cursor for the next page of results').meta(ADDED_V3_1);
|
|
|
|
const RandomSearchBaseSchema = BaseSearchWithResultsSchema.extend({
|
|
withStacked: z.boolean().optional().describe('Include stacked assets'),
|
|
withPeople: z.boolean().optional().describe('Include people data in response'),
|
|
filter: filterField,
|
|
});
|
|
|
|
const RandomSearchSchema = withShapeExclusivity(RandomSearchBaseSchema.strict()).meta({ id: 'RandomSearchDto' });
|
|
|
|
const MetadataSearchSchema = withShapeExclusivity(
|
|
RandomSearchBaseSchema.extend({
|
|
id: z.uuidv4().optional().describe('Filter by asset ID').meta(DEPRECATED_FLAT_FIELD),
|
|
description: z.string().trim().optional().describe('Filter by description text').meta(DEPRECATED_FLAT_FIELD),
|
|
checksum: z.string().optional().describe('Filter by file checksum').meta(DEPRECATED_FLAT_FIELD),
|
|
originalFileName: z.string().trim().optional().describe('Filter by original file name').meta(DEPRECATED_FLAT_FIELD),
|
|
originalPath: z.string().optional().describe('Filter by original file path').meta(DEPRECATED_FLAT_FIELD),
|
|
previewPath: z.string().optional().describe('Filter by preview file path').meta(DEPRECATED_FLAT_FIELD),
|
|
thumbnailPath: z.string().optional().describe('Filter by thumbnail file path').meta(DEPRECATED_FLAT_FIELD),
|
|
encodedVideoPath: z.string().optional().describe('Filter by encoded video file path').meta(DEPRECATED_FLAT_FIELD),
|
|
order: AssetOrderSchema.optional().describe('Sort order').meta(DEPRECATED_FLAT_FIELD),
|
|
page: z.int().min(1).optional().describe('Page number').meta(DEPRECATED_FLAT_FIELD),
|
|
orderBy: SearchOrderSchema.optional().meta(ADDED_V3_1),
|
|
cursor: cursorField,
|
|
}).strict(),
|
|
).meta({ id: 'MetadataSearchDto' });
|
|
|
|
const StatisticsSearchSchema = withShapeExclusivity(
|
|
BaseSearchSchema.extend({
|
|
description: z.string().trim().optional().describe('Filter by description text').meta(DEPRECATED_FLAT_FIELD),
|
|
filter: filterField,
|
|
}).strict(),
|
|
).meta({ id: 'StatisticsSearchDto' });
|
|
|
|
const SmartSearchSchema = withShapeExclusivity(
|
|
BaseSearchWithResultsSchema.extend({
|
|
size: z.int().min(1).max(1000).default(100).describe('Number of results to return'),
|
|
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(DEPRECATED_FLAT_FIELD),
|
|
filter: filterField,
|
|
}).strict(),
|
|
).meta({ id: 'SmartSearchDto' });
|
|
|
|
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')
|
|
.meta(new HistoryBuilder().deprecated('v3.0.0').getExtensions()),
|
|
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(DEPRECATED_FLAT_FIELD),
|
|
nextCursor: z.string().nullable().describe('Cursor for the next page of results').meta(ADDED_V3_1),
|
|
})
|
|
.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) {}
|