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