feat(server): Add album filter to search (#18985)

* - updated dtos
- added inAlbums to search builder
- only check isNotInAlbum if albumIds is blank/empty

* - consider inAlbums as OR

* - make open-api-dart

* - lint & format

* - remove inAlbums groupBy clause

* - merge main open-api

* - make open-api

* - inAlbums filter AND instead of OR
This commit is contained in:
xCJPECKOVERx 2025-06-09 11:11:43 -04:00 committed by GitHub
parent 242817c49a
commit 14d785cec9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 6 deletions

View File

@ -13,6 +13,7 @@ part of openapi.api;
class MetadataSearchDto { class MetadataSearchDto {
/// Returns a new [MetadataSearchDto] instance. /// Returns a new [MetadataSearchDto] instance.
MetadataSearchDto({ MetadataSearchDto({
this.albumIds = const [],
this.checksum, this.checksum,
this.city, this.city,
this.country, this.country,
@ -57,6 +58,8 @@ class MetadataSearchDto {
this.withStacked, this.withStacked,
}); });
List<String> albumIds;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@ -346,6 +349,7 @@ class MetadataSearchDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is MetadataSearchDto && bool operator ==(Object other) => identical(this, other) || other is MetadataSearchDto &&
_deepEquality.equals(other.albumIds, albumIds) &&
other.checksum == checksum && other.checksum == checksum &&
other.city == city && other.city == city &&
other.country == country && other.country == country &&
@ -392,6 +396,7 @@ class MetadataSearchDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(albumIds.hashCode) +
(checksum == null ? 0 : checksum!.hashCode) + (checksum == null ? 0 : checksum!.hashCode) +
(city == null ? 0 : city!.hashCode) + (city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) + (country == null ? 0 : country!.hashCode) +
@ -436,10 +441,11 @@ class MetadataSearchDto {
(withStacked == null ? 0 : withStacked!.hashCode); (withStacked == null ? 0 : withStacked!.hashCode);
@override @override
String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'albumIds'] = this.albumIds;
if (this.checksum != null) { if (this.checksum != null) {
json[r'checksum'] = this.checksum; json[r'checksum'] = this.checksum;
} else { } else {
@ -650,6 +656,9 @@ class MetadataSearchDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MetadataSearchDto( return MetadataSearchDto(
albumIds: json[r'albumIds'] is Iterable
? (json[r'albumIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
checksum: mapValueOfType<String>(json, r'checksum'), checksum: mapValueOfType<String>(json, r'checksum'),
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),

View File

@ -13,6 +13,7 @@ part of openapi.api;
class RandomSearchDto { class RandomSearchDto {
/// Returns a new [RandomSearchDto] instance. /// Returns a new [RandomSearchDto] instance.
RandomSearchDto({ RandomSearchDto({
this.albumIds = const [],
this.city, this.city,
this.country, this.country,
this.createdAfter, this.createdAfter,
@ -46,6 +47,8 @@ class RandomSearchDto {
this.withStacked, this.withStacked,
}); });
List<String> albumIds;
String? city; String? city;
String? country; String? country;
@ -252,6 +255,7 @@ class RandomSearchDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is RandomSearchDto && bool operator ==(Object other) => identical(this, other) || other is RandomSearchDto &&
_deepEquality.equals(other.albumIds, albumIds) &&
other.city == city && other.city == city &&
other.country == country && other.country == country &&
other.createdAfter == createdAfter && other.createdAfter == createdAfter &&
@ -287,6 +291,7 @@ class RandomSearchDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(albumIds.hashCode) +
(city == null ? 0 : city!.hashCode) + (city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) + (country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) +
@ -320,10 +325,11 @@ class RandomSearchDto {
(withStacked == null ? 0 : withStacked!.hashCode); (withStacked == null ? 0 : withStacked!.hashCode);
@override @override
String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'albumIds'] = this.albumIds;
if (this.city != null) { if (this.city != null) {
json[r'city'] = this.city; json[r'city'] = this.city;
} else { } else {
@ -483,6 +489,9 @@ class RandomSearchDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return RandomSearchDto( return RandomSearchDto(
albumIds: json[r'albumIds'] is Iterable
? (json[r'albumIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
createdAfter: mapDateTime(json, r'createdAfter', r''), createdAfter: mapDateTime(json, r'createdAfter', r''),

View File

@ -13,6 +13,7 @@ part of openapi.api;
class SmartSearchDto { class SmartSearchDto {
/// Returns a new [SmartSearchDto] instance. /// Returns a new [SmartSearchDto] instance.
SmartSearchDto({ SmartSearchDto({
this.albumIds = const [],
this.city, this.city,
this.country, this.country,
this.createdAfter, this.createdAfter,
@ -47,6 +48,8 @@ class SmartSearchDto {
this.withExif, this.withExif,
}); });
List<String> albumIds;
String? city; String? city;
String? country; String? country;
@ -256,6 +259,7 @@ class SmartSearchDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SmartSearchDto && bool operator ==(Object other) => identical(this, other) || other is SmartSearchDto &&
_deepEquality.equals(other.albumIds, albumIds) &&
other.city == city && other.city == city &&
other.country == country && other.country == country &&
other.createdAfter == createdAfter && other.createdAfter == createdAfter &&
@ -292,6 +296,7 @@ class SmartSearchDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(albumIds.hashCode) +
(city == null ? 0 : city!.hashCode) + (city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) + (country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) +
@ -326,10 +331,11 @@ class SmartSearchDto {
(withExif == null ? 0 : withExif!.hashCode); (withExif == null ? 0 : withExif!.hashCode);
@override @override
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'albumIds'] = this.albumIds;
if (this.city != null) { if (this.city != null) {
json[r'city'] = this.city; json[r'city'] = this.city;
} else { } else {
@ -490,6 +496,9 @@ class SmartSearchDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return SmartSearchDto( return SmartSearchDto(
albumIds: json[r'albumIds'] is Iterable
? (json[r'albumIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
createdAfter: mapDateTime(json, r'createdAfter', r''), createdAfter: mapDateTime(json, r'createdAfter', r''),

View File

@ -13,6 +13,7 @@ part of openapi.api;
class StatisticsSearchDto { class StatisticsSearchDto {
/// Returns a new [StatisticsSearchDto] instance. /// Returns a new [StatisticsSearchDto] instance.
StatisticsSearchDto({ StatisticsSearchDto({
this.albumIds = const [],
this.city, this.city,
this.country, this.country,
this.createdAfter, this.createdAfter,
@ -42,6 +43,8 @@ class StatisticsSearchDto {
this.visibility, this.visibility,
}); });
List<String> albumIds;
String? city; String? city;
String? country; String? country;
@ -214,6 +217,7 @@ class StatisticsSearchDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is StatisticsSearchDto && bool operator ==(Object other) => identical(this, other) || other is StatisticsSearchDto &&
_deepEquality.equals(other.albumIds, albumIds) &&
other.city == city && other.city == city &&
other.country == country && other.country == country &&
other.createdAfter == createdAfter && other.createdAfter == createdAfter &&
@ -245,6 +249,7 @@ class StatisticsSearchDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(albumIds.hashCode) +
(city == null ? 0 : city!.hashCode) + (city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) + (country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) +
@ -274,10 +279,11 @@ class StatisticsSearchDto {
(visibility == null ? 0 : visibility!.hashCode); (visibility == null ? 0 : visibility!.hashCode);
@override @override
String toString() => 'StatisticsSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'albumIds'] = this.albumIds;
if (this.city != null) { if (this.city != null) {
json[r'city'] = this.city; json[r'city'] = this.city;
} else { } else {
@ -417,6 +423,9 @@ class StatisticsSearchDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return StatisticsSearchDto( return StatisticsSearchDto(
albumIds: json[r'albumIds'] is Iterable
? (json[r'albumIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
createdAfter: mapDateTime(json, r'createdAfter', r''), createdAfter: mapDateTime(json, r'createdAfter', r''),

View File

@ -10866,6 +10866,13 @@
}, },
"MetadataSearchDto": { "MetadataSearchDto": {
"properties": { "properties": {
"albumIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"checksum": { "checksum": {
"type": "string" "type": "string"
}, },
@ -11785,6 +11792,13 @@
}, },
"RandomSearchDto": { "RandomSearchDto": {
"properties": { "properties": {
"albumIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"city": { "city": {
"nullable": true, "nullable": true,
"type": "string" "type": "string"
@ -12833,6 +12847,13 @@
}, },
"SmartSearchDto": { "SmartSearchDto": {
"properties": { "properties": {
"albumIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"city": { "city": {
"nullable": true, "nullable": true,
"type": "string" "type": "string"
@ -13029,6 +13050,13 @@
}, },
"StatisticsSearchDto": { "StatisticsSearchDto": {
"properties": { "properties": {
"albumIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"city": { "city": {
"nullable": true, "nullable": true,
"type": "string" "type": "string"

View File

@ -853,6 +853,7 @@ export type SearchExploreResponseDto = {
items: SearchExploreItem[]; items: SearchExploreItem[];
}; };
export type MetadataSearchDto = { export type MetadataSearchDto = {
albumIds?: string[];
checksum?: string; checksum?: string;
city?: string | null; city?: string | null;
country?: string | null; country?: string | null;
@ -929,6 +930,7 @@ export type PlacesResponseDto = {
name: string; name: string;
}; };
export type RandomSearchDto = { export type RandomSearchDto = {
albumIds?: string[];
city?: string | null; city?: string | null;
country?: string | null; country?: string | null;
createdAfter?: string; createdAfter?: string;
@ -962,6 +964,7 @@ export type RandomSearchDto = {
withStacked?: boolean; withStacked?: boolean;
}; };
export type SmartSearchDto = { export type SmartSearchDto = {
albumIds?: string[];
city?: string | null; city?: string | null;
country?: string | null; country?: string | null;
createdAfter?: string; createdAfter?: string;
@ -996,6 +999,7 @@ export type SmartSearchDto = {
withExif?: boolean; withExif?: boolean;
}; };
export type StatisticsSearchDto = { export type StatisticsSearchDto = {
albumIds?: string[];
city?: string | null; city?: string | null;
country?: string | null; country?: string | null;
createdAfter?: string; createdAfter?: string;

View File

@ -95,6 +95,9 @@ class BaseSearchDto {
@ValidateUUID({ each: true, optional: true }) @ValidateUUID({ each: true, optional: true })
tagIds?: string[]; tagIds?: string[];
@ValidateUUID({ each: true, optional: true })
albumIds?: string[];
@Optional() @Optional()
@IsInt() @IsInt()
@Max(5) @Max(5)

View File

@ -91,6 +91,10 @@ export interface SearchTagOptions {
tagIds?: string[]; tagIds?: string[];
} }
export interface SearchAlbumOptions {
albumIds?: string[];
}
export interface SearchOrderOptions { export interface SearchOrderOptions {
orderDirection?: 'asc' | 'desc'; orderDirection?: 'asc' | 'desc';
} }
@ -108,7 +112,8 @@ type BaseAssetSearchOptions = SearchDateOptions &
SearchStatusOptions & SearchStatusOptions &
SearchUserIdOptions & SearchUserIdOptions &
SearchPeopleOptions & SearchPeopleOptions &
SearchTagOptions; SearchTagOptions &
SearchAlbumOptions;
export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions; export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;

View File

@ -228,6 +228,20 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds:
); );
} }
export function inAlbums<O>(qb: SelectQueryBuilder<DB, 'assets', O>, albumIds: string[]) {
return qb.innerJoin(
(eb) =>
eb
.selectFrom('albums_assets_assets')
.select('assetsId')
.where('albumsId', '=', anyUuid(albumIds!))
.groupBy('assetsId')
.having((eb) => eb.fn.count('albumsId').distinct(), '=', albumIds.length)
.as('has_album'),
(join) => join.onRef('has_album.assetsId', '=', 'assets.id'),
);
}
export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) { export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) {
return qb.innerJoin( return qb.innerJoin(
(eb) => (eb) =>
@ -292,6 +306,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.withPlugin(joinDeduplicationPlugin) .withPlugin(joinDeduplicationPlugin)
.selectFrom('assets') .selectFrom('assets')
.where('assets.visibility', '=', visibility) .where('assets.visibility', '=', visibility)
.$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!))
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
.$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
@ -368,7 +383,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.$if(options.isMotion !== undefined, (qb) => .$if(options.isMotion !== undefined, (qb) =>
qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null),
) )
.$if(!!options.isNotInAlbum, (qb) => .$if(!!options.isNotInAlbum && (!options.albumIds || options.albumIds.length === 0), (qb) =>
qb.where((eb) => qb.where((eb) =>
eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))), eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))),
), ),