mirror of
https://github.com/immich-app/immich.git
synced 2025-03-24 17:05:55 +01:00
fix(server): query fixes (#15509)
This commit is contained in:
parent
7b882b35e5
commit
49a6961ec6
15 changed files with 275 additions and 165 deletions
server/src
entities
interfaces
queries
repositories
album.repository.tsasset.repository.tslibrary.repository.tsmemory.repository.tsperson.repository.tstrash.repository.ts
services
|
@ -1,6 +1,6 @@
|
|||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, Selectable, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Assets, DB } from 'src/db';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
|
@ -181,15 +181,13 @@ export class AssetEntity {
|
|||
}
|
||||
|
||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
return qb
|
||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'));
|
||||
return qb.leftJoin('exif', 'assets.id', 'exif.assetId').select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo'));
|
||||
}
|
||||
|
||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
return qb
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'));
|
||||
.select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo'));
|
||||
}
|
||||
|
||||
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
|
@ -268,48 +266,6 @@ export function withLibrary(eb: ExpressionBuilder<DB, 'assets'>) {
|
|||
);
|
||||
}
|
||||
|
||||
export function withStackedAssets<O>(qb: SelectQueryBuilder<DB, 'assets' | 'asset_stack', O>) {
|
||||
return qb
|
||||
.innerJoinLateral(
|
||||
(eb: ExpressionBuilder<DB, 'assets' | 'asset_stack'>) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.select((eb) => eb.fn<Selectable<Assets>[]>('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.whereRef('asset_stack.id', '=', 'stacked.stackId')
|
||||
.whereRef('asset_stack.primaryAssetId', '!=', 'stacked.id')
|
||||
.as('s'),
|
||||
(join) =>
|
||||
join.on((eb) =>
|
||||
eb.or([eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), eb('assets.stackId', 'is', null)]),
|
||||
),
|
||||
)
|
||||
.select('s.assets');
|
||||
}
|
||||
|
||||
export function withStack<O>(
|
||||
qb: SelectQueryBuilder<DB, 'assets', O>,
|
||||
{ assets, count }: { assets: boolean; count: boolean },
|
||||
) {
|
||||
return qb
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll('asset_stack')
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id')
|
||||
.$if(assets, withStackedAssets)
|
||||
.$if(count, (qb) =>
|
||||
// There is no `selectNoFrom` method for expression builders
|
||||
qb.select(
|
||||
sql`(select count(*) as "assetCount" where "asset_stack"."id" = "assets"."stackId")`.as('assetCount'),
|
||||
),
|
||||
)
|
||||
.as('stacked_assets'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn('to_jsonb', [eb.table('stacked_assets')]).as('stack'));
|
||||
}
|
||||
|
||||
export function withAlbums<O>(qb: SelectQueryBuilder<DB, 'assets', O>, { albumId }: { albumId?: string }) {
|
||||
return qb
|
||||
.select((eb) =>
|
||||
|
@ -352,6 +308,18 @@ 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 */
|
||||
|
|
|
@ -7,7 +7,7 @@ export const IMemoryRepository = 'IMemoryRepository';
|
|||
|
||||
export interface IMemoryRepository extends IBulkAsset {
|
||||
search(ownerId: string): Promise<MemoryEntity[]>;
|
||||
get(id: string): Promise<MemoryEntity | null>;
|
||||
get(id: string): Promise<MemoryEntity | undefined>;
|
||||
create(
|
||||
memory: Omit<Insertable<Memories>, 'data'> & { data: OnThisDayData },
|
||||
assetIds: Set<string>,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Insertable, Selectable, Updateable } from 'kysely';
|
||||
import { AssetFaces, FaceSearch, Person } from 'src/db';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
|
@ -49,7 +49,7 @@ export interface DeleteFacesOptions {
|
|||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export type SelectFaceOptions = Partial<{ [K in keyof AssetFaceEntity]: boolean }>;
|
||||
export type SelectFaceOptions = (keyof Selectable<AssetFaces>)[];
|
||||
|
||||
export interface IPersonRepository {
|
||||
getAll(options?: Partial<PersonEntity>): AsyncIterableIterator<PersonEntity>;
|
||||
|
@ -74,10 +74,10 @@ export interface IPersonRepository {
|
|||
id: string,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | null>;
|
||||
): Promise<AssetFaceEntity | undefined>;
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | undefined>;
|
||||
getStatistics(personId: string): Promise<PersonStatistics>;
|
||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||
|
|
|
@ -82,11 +82,31 @@ select
|
|||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
) as "sharedLinks",
|
||||
(
|
||||
select
|
||||
json_agg("asset") as "assets"
|
||||
from
|
||||
(
|
||||
select
|
||||
"assets".*,
|
||||
to_json("exif") as "exifInfo"
|
||||
from
|
||||
"assets"
|
||||
inner join "exif" on "assets"."id" = "exif"."assetId"
|
||||
inner join "albums_assets_assets" on "albums_assets_assets"."assetsId" = "assets"."id"
|
||||
where
|
||||
"albums_assets_assets"."albumsId" = "albums"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isArchived" = $1
|
||||
order by
|
||||
"assets"."fileCreatedAt" desc
|
||||
) as "asset"
|
||||
) as "assets"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"albums"."id" = $1
|
||||
"albums"."id" = $2
|
||||
and "albums"."deletedAt" is null
|
||||
|
||||
-- AlbumRepository.getByAssetId
|
||||
|
|
|
@ -23,7 +23,7 @@ with
|
|||
)
|
||||
select
|
||||
"a".*,
|
||||
to_jsonb("exif") as "exifInfo"
|
||||
to_json("exif") as "exifInfo"
|
||||
from
|
||||
"today"
|
||||
inner join lateral (
|
||||
|
@ -56,7 +56,7 @@ select
|
|||
(
|
||||
(now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date
|
||||
) / 365 as "yearsAgo",
|
||||
jsonb_agg("res") as "assets"
|
||||
json_agg("res") as "assets"
|
||||
from
|
||||
"res"
|
||||
group by
|
||||
|
@ -109,34 +109,28 @@ select
|
|||
"assets"."id" = "tag_asset"."assetsId"
|
||||
) as agg
|
||||
) as "tags",
|
||||
to_jsonb("exif") as "exifInfo",
|
||||
to_jsonb("stacked_assets") as "stack"
|
||||
to_json("exif") as "exifInfo",
|
||||
to_json("stacked_assets") as "stack"
|
||||
from
|
||||
"assets"
|
||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||
left join lateral (
|
||||
select
|
||||
"asset_stack".*,
|
||||
"s"."assets"
|
||||
array_agg("stacked") as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
inner join lateral (
|
||||
select
|
||||
array_agg("stacked") as "assets"
|
||||
from
|
||||
"assets" as "stacked"
|
||||
where
|
||||
"asset_stack"."id" = "stacked"."stackId"
|
||||
and "asset_stack"."primaryAssetId" != "stacked"."id"
|
||||
) as "s" on (
|
||||
"asset_stack"."primaryAssetId" = "assets"."id"
|
||||
or "assets"."stackId" is null
|
||||
)
|
||||
"assets" as "stacked"
|
||||
where
|
||||
"assets"."stackId" = "asset_stack"."id"
|
||||
) as "stacked_assets" on true
|
||||
"stacked"."stackId" = "asset_stack"."id"
|
||||
and "stacked"."id" != "asset_stack"."primaryAssetId"
|
||||
and "stacked"."deletedAt" is null
|
||||
and "stacked"."isArchived" = $1
|
||||
group by
|
||||
"asset_stack"."id"
|
||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||
where
|
||||
"assets"."id" = any ($1::uuid [])
|
||||
"assets"."id" = any ($2::uuid [])
|
||||
|
||||
-- AssetRepository.deleteAll
|
||||
delete from "assets"
|
||||
|
@ -278,14 +272,33 @@ order by
|
|||
-- AssetRepository.getTimeBucket
|
||||
select
|
||||
"assets".*,
|
||||
to_jsonb("exif") as "exifInfo"
|
||||
to_json("exif") as "exifInfo",
|
||||
to_json("stacked_assets") as "stack"
|
||||
from
|
||||
"assets"
|
||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||
left join lateral (
|
||||
select
|
||||
"asset_stack".*,
|
||||
count("stacked") as "assetCount"
|
||||
from
|
||||
"assets" as "stacked"
|
||||
where
|
||||
"stacked"."stackId" = "asset_stack"."id"
|
||||
and "stacked"."deletedAt" is null
|
||||
and "stacked"."isArchived" = $1
|
||||
group by
|
||||
"asset_stack"."id"
|
||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $1
|
||||
and date_trunc($2, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $3
|
||||
(
|
||||
"asset_stack"."primaryAssetId" = "assets"."id"
|
||||
or "assets"."stackId" is null
|
||||
)
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
|
||||
order by
|
||||
"assets"."localDateTime" desc
|
||||
|
||||
|
@ -368,25 +381,23 @@ limit
|
|||
-- AssetRepository.getAllForUserFullSync
|
||||
select
|
||||
"assets".*,
|
||||
to_jsonb("exif") as "exifInfo",
|
||||
to_jsonb("stacked_assets") as "stack"
|
||||
to_json("exif") as "exifInfo",
|
||||
to_json("stacked_assets") as "stack"
|
||||
from
|
||||
"assets"
|
||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||
left join lateral (
|
||||
select
|
||||
"asset_stack".*,
|
||||
(
|
||||
select
|
||||
count(*) as "assetCount"
|
||||
where
|
||||
"asset_stack"."id" = "assets"."stackId"
|
||||
) as "assetCount"
|
||||
count("stacked") as "assetCount"
|
||||
from
|
||||
"asset_stack"
|
||||
"assets" as "stacked"
|
||||
where
|
||||
"assets"."stackId" = "asset_stack"."id"
|
||||
) as "stacked_assets" on true
|
||||
"stacked"."stackId" = "asset_stack"."id"
|
||||
group by
|
||||
"asset_stack"."id"
|
||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||
where
|
||||
"assets"."ownerId" = $1::uuid
|
||||
and "isVisible" = $2
|
||||
|
@ -400,25 +411,23 @@ limit
|
|||
-- AssetRepository.getChangedDeltaSync
|
||||
select
|
||||
"assets".*,
|
||||
to_jsonb("exif") as "exifInfo",
|
||||
to_jsonb("stacked_assets") as "stack"
|
||||
to_json("exif") as "exifInfo",
|
||||
to_json("stacked_assets") as "stack"
|
||||
from
|
||||
"assets"
|
||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||
left join lateral (
|
||||
select
|
||||
"asset_stack".*,
|
||||
(
|
||||
select
|
||||
count(*) as "assetCount"
|
||||
where
|
||||
"asset_stack"."id" = "assets"."stackId"
|
||||
) as "assetCount"
|
||||
count("stacked") as "assetCount"
|
||||
from
|
||||
"asset_stack"
|
||||
"assets" as "stacked"
|
||||
where
|
||||
"assets"."stackId" = "asset_stack"."id"
|
||||
) as "stacked_assets" on true
|
||||
"stacked"."stackId" = "asset_stack"."id"
|
||||
group by
|
||||
"asset_stack"."id"
|
||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||
where
|
||||
"assets"."ownerId" = any ($1::uuid [])
|
||||
and "isVisible" = $2
|
||||
|
|
|
@ -112,7 +112,7 @@ order by
|
|||
|
||||
-- LibraryRepository.getStatistics
|
||||
select
|
||||
count("assets"."id") filter (
|
||||
count(*) filter (
|
||||
where
|
||||
(
|
||||
"assets"."type" = $1
|
||||
|
@ -130,8 +130,17 @@ select
|
|||
from
|
||||
"libraries"
|
||||
inner join "assets" on "assets"."libraryId" = "libraries"."id"
|
||||
inner join "exif" on "exif"."assetId" = "assets"."id"
|
||||
left join "exif" on "exif"."assetId" = "assets"."id"
|
||||
where
|
||||
"libraries"."id" = $6
|
||||
group by
|
||||
"libraries"."id"
|
||||
select
|
||||
0::int as "photos",
|
||||
0::int as "videos",
|
||||
0::int as "usage",
|
||||
0::int as "total"
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
"libraries"."id" = $1
|
||||
|
|
|
@ -14,7 +14,7 @@ where
|
|||
-- ViewRepository.getAssetsByOriginalPath
|
||||
select
|
||||
"assets".*,
|
||||
to_jsonb("exif") as "exifInfo"
|
||||
to_json("exif") as "exifInfo"
|
||||
from
|
||||
"assets"
|
||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
|
@ -8,7 +7,6 @@ import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/
|
|||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
const userColumns = [
|
||||
'id',
|
||||
|
@ -64,6 +62,8 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
|||
.select((eb) => eb.fn.toJson('exif').as('exifInfo'))
|
||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.isArchived', '=', false)
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.as('asset'),
|
||||
)
|
||||
|
@ -73,12 +73,9 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
|||
|
||||
@Injectable()
|
||||
export class AlbumRepository implements IAlbumRepository {
|
||||
constructor(
|
||||
@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
) {}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}] })
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
withLibrary,
|
||||
withOwner,
|
||||
withSmartSearch,
|
||||
withStack,
|
||||
withTagId,
|
||||
withTags,
|
||||
} from 'src/entities/asset.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
|
@ -122,13 +122,13 @@ export class AssetRepository implements IAssetRepository {
|
|||
),
|
||||
)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.limit(10)
|
||||
.limit(20)
|
||||
.as('a'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.innerJoin('exif', 'a.id', 'exif.assetId')
|
||||
.selectAll('a')
|
||||
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')),
|
||||
.select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')),
|
||||
)
|
||||
.selectFrom('res')
|
||||
.select(
|
||||
|
@ -136,7 +136,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
'yearsAgo',
|
||||
),
|
||||
)
|
||||
.select((eb) => eb.fn('jsonb_agg', [eb.table('res')]).as('assets'))
|
||||
.select((eb) => eb.fn.jsonAgg(eb.table('res')).as('assets'))
|
||||
.groupBy(sql`("localDateTime" at time zone 'UTC')::date`)
|
||||
.orderBy(sql`("localDateTime" at time zone 'UTC')::date`, 'desc')
|
||||
.limit(10)
|
||||
|
@ -159,7 +159,29 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!library, (qb) => qb.select(withLibrary))
|
||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
||||
.$if(!!smartSearch, withSmartSearch)
|
||||
.$if(!!stack, (qb) => withStack(qb, { assets: !!stack!.assets, count: false }))
|
||||
.$if(!!stack, (qb) =>
|
||||
qb
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack')))
|
||||
.$if(!!stack!.assets, (qb) =>
|
||||
qb
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.where('stacked.isArchived', '=', false)
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
),
|
||||
)
|
||||
.$if(!!tags, (qb) => qb.select(withTags))
|
||||
.execute();
|
||||
|
||||
|
@ -175,7 +197,22 @@ export class AssetRepository implements IAssetRepository {
|
|||
.select(withFacesAndPeople)
|
||||
.select(withTags)
|
||||
.$call(withExif)
|
||||
.$call((qb) => withStack(qb, { assets: true, count: false }))
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.where('stacked.isArchived', '=', false)
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.where('assets.id', '=', anyUuid(ids))
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
}
|
||||
|
@ -287,19 +324,25 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!stack, (qb) =>
|
||||
qb
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
.$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack')))
|
||||
.$if(!!stack!.assets, (qb) =>
|
||||
qb
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.where('stacked.isArchived', '=', false)
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
.$if(!!tags, (qb) => qb.select(withTags))
|
||||
|
@ -567,7 +610,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
||||
.$if(!!options.isDuplicate, (qb) =>
|
||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||
),
|
||||
)
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
||||
)
|
||||
.selectFrom('assets')
|
||||
.select('timeBucket')
|
||||
|
@ -583,7 +627,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH }] })
|
||||
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
|
||||
async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
|
||||
return hasPeople(this.db, options.personId ? [options.personId] : undefined)
|
||||
.selectAll('assets')
|
||||
|
@ -592,12 +636,33 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
|
||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||
.$if(!!options.withStacked, (qb) => withStack(qb, { assets: true, count: false })) // TODO: optimize this; it's a huge performance hit
|
||||
.$if(!!options.withStacked, (qb) =>
|
||||
qb
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.where((eb) =>
|
||||
eb.or([eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), eb('assets.stackId', 'is', null)]),
|
||||
)
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.where('stacked.isArchived', '=', false)
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
)
|
||||
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||
)
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
|
@ -689,7 +754,19 @@ export class AssetRepository implements IAssetRepository {
|
|||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.$call(withExif)
|
||||
.$call((qb) => withStack(qb, { assets: false, count: true }))
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.where('assets.ownerId', '=', asUuid(ownerId))
|
||||
.where('isVisible', '=', true)
|
||||
.where('updatedAt', '<=', updatedUntil)
|
||||
|
@ -705,7 +782,19 @@ export class AssetRepository implements IAssetRepository {
|
|||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.$call(withExif)
|
||||
.$call((qb) => withStack(qb, { assets: false, count: true }))
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
||||
.where('isVisible', '=', true)
|
||||
.where('updatedAt', '>', options.updatedAfter)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, Libraries } from 'src/db';
|
||||
|
@ -100,10 +100,10 @@ export class LibraryRepository implements ILibraryRepository {
|
|||
const stats = await this.db
|
||||
.selectFrom('libraries')
|
||||
.innerJoin('assets', 'assets.libraryId', 'libraries.id')
|
||||
.innerJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.count('assets.id')
|
||||
.countAll()
|
||||
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)]))
|
||||
.as('photos'),
|
||||
)
|
||||
|
@ -118,8 +118,17 @@ export class LibraryRepository implements ILibraryRepository {
|
|||
.where('libraries.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
|
||||
// possibly a new library with 0 assets
|
||||
if (!stats) {
|
||||
return;
|
||||
const zero = sql<number>`0::int`;
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.select(zero.as('photos'))
|
||||
.select(zero.as('videos'))
|
||||
.select(zero.as('usage'))
|
||||
.select(zero.as('total'))
|
||||
.where('libraries.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -22,8 +22,8 @@ export class MemoryRepository implements IMemoryRepository {
|
|||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
get(id: string): Promise<MemoryEntity | null> {
|
||||
return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise<MemoryEntity | null>;
|
||||
get(id: string): Promise<MemoryEntity | undefined> {
|
||||
return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise<MemoryEntity | undefined>;
|
||||
}
|
||||
|
||||
async create(memory: Insertable<Memories>, assetIds: Set<string>): Promise<MemoryEntity> {
|
||||
|
@ -71,6 +71,10 @@ export class MemoryRepository implements IMemoryRepository {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
async addAssetIds(id: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.insertInto('memories_assets_assets')
|
||||
.values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId })))
|
||||
|
@ -80,6 +84,10 @@ export class MemoryRepository implements IMemoryRepository {
|
|||
@Chunked({ paramIndex: 1 })
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
async removeAssetIds(id: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.deleteFrom('memories_assets_assets')
|
||||
.where('memoriesId', '=', id)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, SelectExpression, sql } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
|
@ -212,22 +211,16 @@ export class PersonRepository implements IPersonRepository {
|
|||
id: string,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | null> {
|
||||
return (this.db
|
||||
): Promise<AssetFaceEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.$if(!!select, (qb) =>
|
||||
qb.select(
|
||||
Object.keys(
|
||||
_.omitBy({ ...select!, faceSearch: undefined, asset: undefined }, _.isUndefined),
|
||||
) as SelectExpression<DB, 'asset_faces'>[],
|
||||
),
|
||||
)
|
||||
.$if(!!select, (qb) => qb.select(select!))
|
||||
.$if(!select, (qb) => qb.selectAll('asset_faces'))
|
||||
.select(withPerson)
|
||||
.select(withAsset)
|
||||
.$if(!!relations?.faceSearch, (qb) => qb.select(withFaceSearch))
|
||||
.where('asset_faces.id', '=', id)
|
||||
.executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
|
||||
.executeTakeFirst() as Promise<AssetFaceEntity | undefined>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
|
@ -335,6 +328,10 @@ export class PersonRepository implements IPersonRepository {
|
|||
}
|
||||
|
||||
async createAll(people: Insertable<Person>[]): Promise<string[]> {
|
||||
if (people.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await this.db.insertInto('person').values(people).returningAll().execute();
|
||||
return results.map(({ id }) => id);
|
||||
}
|
||||
|
@ -387,8 +384,12 @@ export class PersonRepository implements IPersonRepository {
|
|||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
||||
@ChunkedArray()
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
|
||||
const { assetIds, personIds }: { assetIds: string[]; personIds: string[] } = { assetIds: [], personIds: [] };
|
||||
if (ids.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const assetIds: string[] = [];
|
||||
const personIds: string[] = [];
|
||||
for (const { assetId, personId } of ids) {
|
||||
assetIds.push(assetId);
|
||||
personIds.push(personId);
|
||||
|
@ -405,12 +406,12 @@ export class PersonRepository implements IPersonRepository {
|
|||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
|
||||
return (this.db
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.where('asset_faces.personId', '=', personId)
|
||||
.executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
|
||||
.executeTakeFirst() as Promise<AssetFaceEntity | undefined>;
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
|
|
|
@ -38,6 +38,10 @@ export class TrashRepository implements ITrashRepository {
|
|||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
async restoreAll(ids: string[]): Promise<number> {
|
||||
if (ids.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { numUpdatedRows } = await this.db
|
||||
.updateTable('assets')
|
||||
.where('status', '=', AssetStatus.TRASHED)
|
||||
|
|
|
@ -368,7 +368,6 @@ describe(PersonService.name, () => {
|
|||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
personMock.getRandomFace.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.reassignFacesById(authStub.admin, personStub.noName.id, {
|
||||
id: faceStub.face1.id,
|
||||
|
@ -391,7 +390,6 @@ describe(PersonService.name, () => {
|
|||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
personMock.getRandomFace.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.reassignFacesById(authStub.admin, personStub.noName.id, {
|
||||
id: faceStub.face1.id,
|
||||
|
@ -771,8 +769,6 @@ describe(PersonService.name, () => {
|
|||
|
||||
describe('handleRecognizeFaces', () => {
|
||||
it('should fail if face does not exist', async () => {
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(null);
|
||||
|
||||
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED);
|
||||
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
|
|
|
@ -145,7 +145,7 @@ export class PersonService extends BaseService {
|
|||
for (const personId of changeFeaturePhoto) {
|
||||
const assetFace = await this.personRepository.getRandomFace(personId);
|
||||
|
||||
if (assetFace !== null) {
|
||||
if (assetFace) {
|
||||
await this.personRepository.update({ id: personId, faceAssetId: assetFace.id });
|
||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } });
|
||||
}
|
||||
|
@ -444,7 +444,7 @@ export class PersonService extends BaseService {
|
|||
const face = await this.personRepository.getFaceByIdWithAssets(
|
||||
id,
|
||||
{ person: true, asset: true, faceSearch: true },
|
||||
{ id: true, personId: true, sourceType: true, faceSearch: true },
|
||||
['id', 'personId', 'sourceType'],
|
||||
);
|
||||
if (!face || !face.asset) {
|
||||
this.logger.warn(`Face ${id} not found`);
|
||||
|
@ -544,7 +544,7 @@ export class PersonService extends BaseService {
|
|||
}
|
||||
|
||||
const face = await this.personRepository.getFaceByIdWithAssets(person.faceAssetId);
|
||||
if (face === null) {
|
||||
if (!face) {
|
||||
this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue