mirror of
https://github.com/immich-app/immich
synced 2025-06-08 06:00:59 +00:00
chore: remove asset entity (#17703)
This commit is contained in:
parent
52ae06c119
commit
dd1fcd5be5
@ -1,7 +1,6 @@
|
|||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
import { Albums, AssetJobStatus as DatabaseAssetJobStatus, Exif as DatabaseExif } from 'src/db';
|
import { Albums, Exif as DatabaseExif } from 'src/db';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
@ -265,10 +264,6 @@ export type AssetFace = {
|
|||||||
person?: Person | null;
|
person?: Person | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssetJobStatus = Selectable<DatabaseAssetJobStatus> & {
|
|
||||||
asset: AssetEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
||||||
|
|
||||||
export const columns = {
|
export const columns = {
|
||||||
|
@ -1,272 +0,0 @@
|
|||||||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
||||||
import { AssetFace, AssetFile, AssetJobStatus, columns, Exif, Person, Stack, Tag, User } from 'src/database';
|
|
||||||
import { DB } from 'src/db';
|
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
|
||||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
|
||||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
|
||||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
|
||||||
import { anyUuid, asUuid, toJson } from 'src/utils/database';
|
|
||||||
|
|
||||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
|
||||||
|
|
||||||
export class AssetEntity {
|
|
||||||
id!: string;
|
|
||||||
deviceAssetId!: string;
|
|
||||||
owner!: User;
|
|
||||||
ownerId!: string;
|
|
||||||
libraryId?: string | null;
|
|
||||||
deviceId!: string;
|
|
||||||
type!: AssetType;
|
|
||||||
status!: AssetStatus;
|
|
||||||
originalPath!: string;
|
|
||||||
files!: AssetFile[];
|
|
||||||
thumbhash!: Buffer | null;
|
|
||||||
encodedVideoPath!: string | null;
|
|
||||||
createdAt!: Date;
|
|
||||||
updatedAt!: Date;
|
|
||||||
updateId?: string;
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
fileCreatedAt!: Date;
|
|
||||||
localDateTime!: Date;
|
|
||||||
fileModifiedAt!: Date;
|
|
||||||
isFavorite!: boolean;
|
|
||||||
isArchived!: boolean;
|
|
||||||
isExternal!: boolean;
|
|
||||||
isOffline!: boolean;
|
|
||||||
checksum!: Buffer; // sha1 checksum
|
|
||||||
duration!: string | null;
|
|
||||||
isVisible!: boolean;
|
|
||||||
livePhotoVideo!: MapAsset | null;
|
|
||||||
livePhotoVideoId!: string | null;
|
|
||||||
originalFileName!: string;
|
|
||||||
sidecarPath!: string | null;
|
|
||||||
exifInfo?: Exif;
|
|
||||||
tags?: Tag[];
|
|
||||||
faces!: AssetFace[];
|
|
||||||
stackId?: string | null;
|
|
||||||
stack?: Stack | null;
|
|
||||||
jobStatus?: AssetJobStatus;
|
|
||||||
duplicateId!: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO come up with a better query that only selects the fields we need
|
|
||||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|
||||||
return qb
|
|
||||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif | null>().as('exifInfo'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|
||||||
return qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif>().as('exifInfo'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
|
||||||
return qb
|
|
||||||
.leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
|
||||||
.select((eb) => toJson(eb, 'smart_search').as('smartSearch'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withFaces(eb: ExpressionBuilder<DB, 'assets'>, withDeletedFace?: boolean) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_faces')
|
|
||||||
.selectAll('asset_faces')
|
|
||||||
.whereRef('asset_faces.assetId', '=', 'assets.id')
|
|
||||||
.$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)),
|
|
||||||
).as('faces');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withFiles(eb: ExpressionBuilder<DB, 'assets'>, type?: AssetFileType) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_files')
|
|
||||||
.select(columns.assetFiles)
|
|
||||||
.whereRef('asset_files.assetId', '=', 'assets.id')
|
|
||||||
.$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)),
|
|
||||||
).as('files');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withFacesAndPeople(eb: ExpressionBuilder<DB, 'assets'>, withDeletedFace?: boolean) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_faces')
|
|
||||||
.leftJoinLateral(
|
|
||||||
(eb) =>
|
|
||||||
eb.selectFrom('person').selectAll('person').whereRef('asset_faces.personId', '=', 'person.id').as('person'),
|
|
||||||
(join) => join.onTrue(),
|
|
||||||
)
|
|
||||||
.selectAll('asset_faces')
|
|
||||||
.select((eb) => eb.table('person').$castTo<Person>().as('person'))
|
|
||||||
.whereRef('asset_faces.assetId', '=', 'assets.id')
|
|
||||||
.$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)),
|
|
||||||
).as('faces');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds: string[]) {
|
|
||||||
return qb.innerJoin(
|
|
||||||
(eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('asset_faces')
|
|
||||||
.select('assetId')
|
|
||||||
.where('personId', '=', anyUuid(personIds!))
|
|
||||||
.where('deletedAt', 'is', null)
|
|
||||||
.groupBy('assetId')
|
|
||||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
|
||||||
.as('has_people'),
|
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) {
|
|
||||||
return qb.innerJoin(
|
|
||||||
(eb) =>
|
|
||||||
eb
|
|
||||||
.selectFrom('tag_asset')
|
|
||||||
.select('assetsId')
|
|
||||||
.innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
|
||||||
.where('tags_closure.id_ancestor', '=', anyUuid(tagIds))
|
|
||||||
.groupBy('assetsId')
|
|
||||||
.having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length)
|
|
||||||
.as('has_tags'),
|
|
||||||
(join) => join.onRef('has_tags.assetsId', '=', 'assets.id'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withOwner(eb: ExpressionBuilder<DB, 'assets'>) {
|
|
||||||
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'assets.ownerId')).as(
|
|
||||||
'owner',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withLibrary(eb: ExpressionBuilder<DB, 'assets'>) {
|
|
||||||
return jsonObjectFrom(
|
|
||||||
eb.selectFrom('libraries').selectAll('libraries').whereRef('libraries.id', '=', 'assets.libraryId'),
|
|
||||||
).as('library');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('tags')
|
|
||||||
.select(columns.tag)
|
|
||||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
|
||||||
.whereRef('assets.id', '=', 'tag_asset.assetsId'),
|
|
||||||
).as('tags');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function truncatedDate<O>(size: TimeBucketSize) {
|
|
||||||
return sql<O>`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {
|
|
||||||
return qb.where((eb) =>
|
|
||||||
eb.exists(
|
|
||||||
eb
|
|
||||||
.selectFrom('tags_closure')
|
|
||||||
.innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
|
||||||
.whereRef('tag_asset.assetsId', '=', 'assets.id')
|
|
||||||
.where('tags_closure.id_ancestor', '=', tagId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
|
||||||
|
|
||||||
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
|
||||||
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
|
||||||
options.isArchived ??= options.withArchived ? undefined : false;
|
|
||||||
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
|
|
||||||
return kysely
|
|
||||||
.withPlugin(joinDeduplicationPlugin)
|
|
||||||
.selectFrom('assets')
|
|
||||||
.selectAll('assets')
|
|
||||||
.$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.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
|
|
||||||
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!))
|
|
||||||
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!))
|
|
||||||
.$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!))
|
|
||||||
.$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!))
|
|
||||||
.$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!))
|
|
||||||
.$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!))
|
|
||||||
.$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!))
|
|
||||||
.$if(options.city !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.city', options.city === null ? 'is' : '=', options.city!),
|
|
||||||
)
|
|
||||||
.$if(options.state !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.state', options.state === null ? 'is' : '=', options.state!),
|
|
||||||
)
|
|
||||||
.$if(options.country !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.country', options.country === null ? 'is' : '=', options.country!),
|
|
||||||
)
|
|
||||||
.$if(options.make !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.make', options.make === null ? 'is' : '=', options.make!),
|
|
||||||
)
|
|
||||||
.$if(options.model !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.model', options.model === null ? 'is' : '=', options.model!),
|
|
||||||
)
|
|
||||||
.$if(options.lensModel !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!),
|
|
||||||
)
|
|
||||||
.$if(options.rating !== undefined, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where('exif.rating', options.rating === null ? 'is' : '=', options.rating!),
|
|
||||||
)
|
|
||||||
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!))
|
|
||||||
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!))
|
|
||||||
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!))
|
|
||||||
.$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!)))
|
|
||||||
.$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!)))
|
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
|
||||||
.$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!))
|
|
||||||
.$if(!!options.originalPath, (qb) =>
|
|
||||||
qb.where(sql`f_unaccent(assets."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`),
|
|
||||||
)
|
|
||||||
.$if(!!options.originalFileName, (qb) =>
|
|
||||||
qb.where(
|
|
||||||
sql`f_unaccent(assets."originalFileName")`,
|
|
||||||
'ilike',
|
|
||||||
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!options.description, (qb) =>
|
|
||||||
qb
|
|
||||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
|
||||||
.where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`),
|
|
||||||
)
|
|
||||||
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
|
||||||
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
|
||||||
.$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
|
|
||||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
|
||||||
.$if(options.isEncoded !== undefined, (qb) =>
|
|
||||||
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
|
|
||||||
)
|
|
||||||
.$if(options.isMotion !== undefined, (qb) =>
|
|
||||||
qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null),
|
|
||||||
)
|
|
||||||
.$if(!!options.isNotInAlbum, (qb) =>
|
|
||||||
qb.where((eb) =>
|
|
||||||
eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.$if(!!options.withExif, withExifInner)
|
|
||||||
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
|
||||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
|
||||||
}
|
|
@ -5,10 +5,9 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { withExifInner, withFaces, withFiles } from 'src/entities/asset.entity';
|
|
||||||
import { AssetFileType } from 'src/enum';
|
import { AssetFileType } from 'src/enum';
|
||||||
import { StorageAsset } from 'src/types';
|
import { StorageAsset } from 'src/types';
|
||||||
import { anyUuid, asUuid } from 'src/utils/database';
|
import { anyUuid, asUuid, withExifInner, withFaces, withFiles } from 'src/utils/database';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetJobRepository {
|
export class AssetJobRepository {
|
||||||
|
@ -6,11 +6,16 @@ import { Stack } from 'src/database';
|
|||||||
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
|
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||||
|
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
|
||||||
import {
|
import {
|
||||||
AssetEntity,
|
anyUuid,
|
||||||
|
asUuid,
|
||||||
hasPeople,
|
hasPeople,
|
||||||
|
removeUndefinedKeys,
|
||||||
searchAssetBuilder,
|
searchAssetBuilder,
|
||||||
truncatedDate,
|
truncatedDate,
|
||||||
|
unnest,
|
||||||
withExif,
|
withExif,
|
||||||
withFaces,
|
withFaces,
|
||||||
withFacesAndPeople,
|
withFacesAndPeople,
|
||||||
@ -20,10 +25,7 @@ import {
|
|||||||
withSmartSearch,
|
withSmartSearch,
|
||||||
withTagId,
|
withTagId,
|
||||||
withTags,
|
withTags,
|
||||||
} from 'src/entities/asset.entity';
|
} from 'src/utils/database';
|
||||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
|
||||||
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
|
|
||||||
import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database';
|
|
||||||
import { globToSqlPattern } from 'src/utils/misc';
|
import { globToSqlPattern } from 'src/utils/misc';
|
||||||
import { PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
import { PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||||
|
|
||||||
@ -128,8 +130,6 @@ export interface AssetGetByChecksumOptions {
|
|||||||
libraryId?: string;
|
libraryId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
|
||||||
|
|
||||||
export interface GetByIdsRelations {
|
export interface GetByIdsRelations {
|
||||||
exifInfo?: boolean;
|
exifInfo?: boolean;
|
||||||
faces?: { person?: boolean; withDeleted?: boolean };
|
faces?: { person?: boolean; withDeleted?: boolean };
|
||||||
@ -493,13 +493,13 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] })
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] })
|
||||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]> {
|
getByChecksums(userId: string, checksums: Buffer[]) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(['id', 'checksum', 'deletedAt'])
|
.select(['id', 'checksum', 'deletedAt'])
|
||||||
.where('ownerId', '=', asUuid(userId))
|
.where('ownerId', '=', asUuid(userId))
|
||||||
.where('checksum', 'in', checksums)
|
.where('checksum', 'in', checksums)
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||||
|
@ -5,9 +5,8 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import { DB, Exif } from 'src/db';
|
import { DB, Exif } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { searchAssetBuilder } from 'src/entities/asset.entity';
|
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { anyUuid, asUuid } from 'src/utils/database';
|
import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database';
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
|
|
||||||
export interface SearchResult<T> {
|
export interface SearchResult<T> {
|
||||||
|
@ -2,8 +2,7 @@ import { Kysely } from 'kysely';
|
|||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { withExif } from 'src/entities/asset.entity';
|
import { asUuid, withExif } from 'src/utils/database';
|
||||||
import { asUuid } from 'src/utils/database';
|
|
||||||
|
|
||||||
export class ViewRepository {
|
export class ViewRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
|
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { assets_status_enum } from 'src/schema/enums';
|
import { assets_status_enum } from 'src/schema/enums';
|
||||||
import { assets_delete_audit } from 'src/schema/functions';
|
import { assets_delete_audit } from 'src/schema/functions';
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||||
|
|
||||||
@Table('assets')
|
@Table('assets')
|
||||||
@UpdatedAtTrigger('assets_updated_at')
|
@UpdatedAtTrigger('assets_updated_at')
|
||||||
|
@ -9,10 +9,10 @@ import { AssetFile } from 'src/database';
|
|||||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum';
|
import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
|
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
@ -820,8 +820,8 @@ describe(AssetMediaService.name, () => {
|
|||||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||||
|
|
||||||
mocks.asset.getByChecksums.mockResolvedValue([
|
mocks.asset.getByChecksums.mockResolvedValue([
|
||||||
{ id: 'asset-1', checksum: file1 } as AssetEntity,
|
{ id: 'asset-1', checksum: file1, deletedAt: null },
|
||||||
{ id: 'asset-2', checksum: file2 } as AssetEntity,
|
{ id: 'asset-2', checksum: file2, deletedAt: null },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -857,7 +857,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||||
|
|
||||||
mocks.asset.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]);
|
mocks.asset.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1, deletedAt: null }]);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.bulkUploadCheck(authStub.admin, {
|
sut.bulkUploadCheck(authStub.admin, {
|
||||||
|
@ -21,13 +21,13 @@ import {
|
|||||||
UploadFieldName,
|
UploadFieldName,
|
||||||
} from 'src/dtos/asset-media.dto';
|
} from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
|
|
||||||
import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { UploadFile } from 'src/types';
|
import { UploadFile } from 'src/types';
|
||||||
import { requireUploadAccess } from 'src/utils/access';
|
import { requireUploadAccess } from 'src/utils/access';
|
||||||
import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||||
|
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||||
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { fromChecksum } from 'src/utils/request';
|
import { fromChecksum } from 'src/utils/request';
|
||||||
|
@ -15,6 +15,14 @@ describe(MemoryService.name, () => {
|
|||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onMemoryCleanup', () => {
|
||||||
|
it('should clean up memories', async () => {
|
||||||
|
mocks.memory.cleanup.mockResolvedValue([]);
|
||||||
|
await sut.onMemoriesCleanup();
|
||||||
|
expect(mocks.memory.cleanup).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
it('should search memories', async () => {
|
it('should search memories', async () => {
|
||||||
const [userId] = newUuids();
|
const [userId] = newUuids();
|
||||||
|
@ -39,6 +39,29 @@ describe(SearchService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('searchPlaces', () => {
|
||||||
|
it('should search places', async () => {
|
||||||
|
mocks.search.searchPlaces.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 42,
|
||||||
|
name: 'my place',
|
||||||
|
latitude: 420,
|
||||||
|
longitude: 69,
|
||||||
|
admin1Code: null,
|
||||||
|
admin1Name: null,
|
||||||
|
admin2Code: null,
|
||||||
|
admin2Name: null,
|
||||||
|
alternateNames: null,
|
||||||
|
countryCode: 'US',
|
||||||
|
modificationDate: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.searchPlaces({ name: 'place' });
|
||||||
|
expect(mocks.search.searchPlaces).toHaveBeenCalledWith('place');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getExploreData', () => {
|
describe('getExploreData', () => {
|
||||||
it('should get assets by city and tag', async () => {
|
it('should get assets by city and tag', async () => {
|
||||||
mocks.asset.getAssetIdByCity.mockResolvedValue({
|
mocks.asset.getAssetIdByCity.mockResolvedValue({
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
|
DeduplicateJoinsPlugin,
|
||||||
Expression,
|
Expression,
|
||||||
ExpressionBuilder,
|
ExpressionBuilder,
|
||||||
ExpressionWrapper,
|
ExpressionWrapper,
|
||||||
|
Kysely,
|
||||||
KyselyConfig,
|
KyselyConfig,
|
||||||
Nullable,
|
Nullable,
|
||||||
Selectable,
|
Selectable,
|
||||||
|
SelectQueryBuilder,
|
||||||
Simplify,
|
Simplify,
|
||||||
sql,
|
sql,
|
||||||
} from 'kysely';
|
} from 'kysely';
|
||||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import postgres, { Notice } from 'postgres';
|
import postgres, { Notice } from 'postgres';
|
||||||
|
import { columns, Exif, Person } from 'src/database';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { AssetFileType } from 'src/enum';
|
||||||
|
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||||
|
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||||
|
|
||||||
type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
||||||
|
|
||||||
@ -112,3 +121,225 @@ export function toJson<DB, TB extends keyof DB & string, T extends TB | Expressi
|
|||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||||
|
// TODO come up with a better query that only selects the fields we need
|
||||||
|
|
||||||
|
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||||
|
return qb
|
||||||
|
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif | null>().as('exifInfo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||||
|
return qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif>().as('exifInfo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||||
|
return qb
|
||||||
|
.leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||||
|
.select((eb) => toJson(eb, 'smart_search').as('smartSearch'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withFaces(eb: ExpressionBuilder<DB, 'assets'>, withDeletedFace?: boolean) {
|
||||||
|
return jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('asset_faces')
|
||||||
|
.selectAll('asset_faces')
|
||||||
|
.whereRef('asset_faces.assetId', '=', 'assets.id')
|
||||||
|
.$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)),
|
||||||
|
).as('faces');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withFiles(eb: ExpressionBuilder<DB, 'assets'>, type?: AssetFileType) {
|
||||||
|
return jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('asset_files')
|
||||||
|
.select(columns.assetFiles)
|
||||||
|
.whereRef('asset_files.assetId', '=', 'assets.id')
|
||||||
|
.$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)),
|
||||||
|
).as('files');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withFacesAndPeople(eb: ExpressionBuilder<DB, 'assets'>, withDeletedFace?: boolean) {
|
||||||
|
return jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('asset_faces')
|
||||||
|
.leftJoinLateral(
|
||||||
|
(eb) =>
|
||||||
|
eb.selectFrom('person').selectAll('person').whereRef('asset_faces.personId', '=', 'person.id').as('person'),
|
||||||
|
(join) => join.onTrue(),
|
||||||
|
)
|
||||||
|
.selectAll('asset_faces')
|
||||||
|
.select((eb) => eb.table('person').$castTo<Person>().as('person'))
|
||||||
|
.whereRef('asset_faces.assetId', '=', 'assets.id')
|
||||||
|
.$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)),
|
||||||
|
).as('faces');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds: string[]) {
|
||||||
|
return qb.innerJoin(
|
||||||
|
(eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('asset_faces')
|
||||||
|
.select('assetId')
|
||||||
|
.where('personId', '=', anyUuid(personIds!))
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.groupBy('assetId')
|
||||||
|
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||||
|
.as('has_people'),
|
||||||
|
(join) => join.onRef('has_people.assetId', '=', 'assets.id'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) {
|
||||||
|
return qb.innerJoin(
|
||||||
|
(eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('tag_asset')
|
||||||
|
.select('assetsId')
|
||||||
|
.innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
||||||
|
.where('tags_closure.id_ancestor', '=', anyUuid(tagIds))
|
||||||
|
.groupBy('assetsId')
|
||||||
|
.having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length)
|
||||||
|
.as('has_tags'),
|
||||||
|
(join) => join.onRef('has_tags.assetsId', '=', 'assets.id'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withOwner(eb: ExpressionBuilder<DB, 'assets'>) {
|
||||||
|
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'assets.ownerId')).as(
|
||||||
|
'owner',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withLibrary(eb: ExpressionBuilder<DB, 'assets'>) {
|
||||||
|
return jsonObjectFrom(
|
||||||
|
eb.selectFrom('libraries').selectAll('libraries').whereRef('libraries.id', '=', 'assets.libraryId'),
|
||||||
|
).as('library');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
|
||||||
|
return jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('tags')
|
||||||
|
.select(columns.tag)
|
||||||
|
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
||||||
|
.whereRef('assets.id', '=', 'tag_asset.assetsId'),
|
||||||
|
).as('tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncatedDate<O>(size: TimeBucketSize) {
|
||||||
|
return sql<O>`date_trunc(${size}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {
|
||||||
|
return qb.where((eb) =>
|
||||||
|
eb.exists(
|
||||||
|
eb
|
||||||
|
.selectFrom('tags_closure')
|
||||||
|
.innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant')
|
||||||
|
.whereRef('tag_asset.assetsId', '=', 'assets.id')
|
||||||
|
.where('tags_closure.id_ancestor', '=', tagId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
||||||
|
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
||||||
|
|
||||||
|
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
||||||
|
options.isArchived ??= options.withArchived ? undefined : false;
|
||||||
|
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
|
||||||
|
return kysely
|
||||||
|
.withPlugin(joinDeduplicationPlugin)
|
||||||
|
.selectFrom('assets')
|
||||||
|
.selectAll('assets')
|
||||||
|
.$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.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
|
||||||
|
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!))
|
||||||
|
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!))
|
||||||
|
.$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!))
|
||||||
|
.$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!))
|
||||||
|
.$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!))
|
||||||
|
.$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!))
|
||||||
|
.$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!))
|
||||||
|
.$if(options.city !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.city', options.city === null ? 'is' : '=', options.city!),
|
||||||
|
)
|
||||||
|
.$if(options.state !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.state', options.state === null ? 'is' : '=', options.state!),
|
||||||
|
)
|
||||||
|
.$if(options.country !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.country', options.country === null ? 'is' : '=', options.country!),
|
||||||
|
)
|
||||||
|
.$if(options.make !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.make', options.make === null ? 'is' : '=', options.make!),
|
||||||
|
)
|
||||||
|
.$if(options.model !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.model', options.model === null ? 'is' : '=', options.model!),
|
||||||
|
)
|
||||||
|
.$if(options.lensModel !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!),
|
||||||
|
)
|
||||||
|
.$if(options.rating !== undefined, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where('exif.rating', options.rating === null ? 'is' : '=', options.rating!),
|
||||||
|
)
|
||||||
|
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!))
|
||||||
|
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!))
|
||||||
|
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!))
|
||||||
|
.$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!)))
|
||||||
|
.$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!)))
|
||||||
|
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||||
|
.$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!))
|
||||||
|
.$if(!!options.originalPath, (qb) =>
|
||||||
|
qb.where(sql`f_unaccent(assets."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`),
|
||||||
|
)
|
||||||
|
.$if(!!options.originalFileName, (qb) =>
|
||||||
|
qb.where(
|
||||||
|
sql`f_unaccent(assets."originalFileName")`,
|
||||||
|
'ilike',
|
||||||
|
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(!!options.description, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`),
|
||||||
|
)
|
||||||
|
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
||||||
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
|
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
||||||
|
.$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
|
||||||
|
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
||||||
|
.$if(options.isEncoded !== undefined, (qb) =>
|
||||||
|
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
|
||||||
|
)
|
||||||
|
.$if(options.isMotion !== undefined, (qb) =>
|
||||||
|
qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null),
|
||||||
|
)
|
||||||
|
.$if(!!options.isNotInAlbum, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(!!options.withExif, withExifInner)
|
||||||
|
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
||||||
|
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user