refactor(server): remove face, person and face search entities (#17535)

* remove face, person and face search entities

update tests and mappers

check if face relation exists

update sql

unused imports

* pr feedback

generate sql, remove unused imports
This commit is contained in:
Mert
2025-04-11 14:44:45 -04:00
committed by GitHub
parent ae6653392e
commit 25f2b9602f
19 changed files with 384 additions and 322 deletions

View File

@@ -1,14 +1,12 @@
import { Injectable } from '@nestjs/common';
import { ExpressionBuilder, Insertable, Kysely, Selectable, sql } from 'kysely';
import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SourceType } from 'src/enum';
import { AssetFileType, SourceType } from 'src/enum';
import { removeUndefinedKeys } from 'src/utils/database';
import { Paginated, PaginationOptions } from 'src/utils/pagination';
import { PaginationOptions } from 'src/utils/pagination';
export interface PersonSearchOptions {
minimumFaceCount: number;
@@ -49,6 +47,19 @@ export interface DeleteFacesOptions {
sourceType: SourceType;
}
export interface GetAllPeopleOptions {
ownerId?: string;
thumbnailPath?: string;
faceAssetId?: string | null;
isHidden?: boolean;
}
export interface GetAllFacesOptions {
personId?: string | null;
assetId?: string;
sourceType?: SourceType;
}
export type UnassignFacesOptions = DeleteFacesOptions;
export type SelectFaceOptions = (keyof Selectable<AssetFaces>)[];
@@ -98,20 +109,13 @@ export class PersonRepository {
await this.vacuum({ reindexVectors: false });
}
@GenerateSql({ params: [[{ id: DummyValue.UUID }]] })
async delete(entities: PersonEntity[]): Promise<void> {
if (entities.length === 0) {
@GenerateSql({ params: [DummyValue.UUID] })
async delete(ids: string[]): Promise<void> {
if (ids.length === 0) {
return;
}
await this.db
.deleteFrom('person')
.where(
'person.id',
'in',
entities.map(({ id }) => id),
)
.execute();
await this.db.deleteFrom('person').where('person.id', 'in', ids).execute();
}
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
@@ -121,7 +125,7 @@ export class PersonRepository {
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
}
getAllFaces(options: Partial<AssetFaceEntity> = {}): AsyncIterableIterator<AssetFaceEntity> {
getAllFaces(options: GetAllFacesOptions = {}) {
return this.db
.selectFrom('asset_faces')
.selectAll('asset_faces')
@@ -130,10 +134,10 @@ export class PersonRepository {
.$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!))
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
.where('asset_faces.deletedAt', 'is', null)
.stream() as AsyncIterableIterator<AssetFaceEntity>;
.stream();
}
getAll(options: Partial<PersonEntity> = {}): AsyncIterableIterator<PersonEntity> {
getAll(options: GetAllPeopleOptions = {}) {
return this.db
.selectFrom('person')
.selectAll('person')
@@ -142,15 +146,11 @@ export class PersonRepository {
.$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null))
.$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!))
.$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!))
.stream() as AsyncIterableIterator<PersonEntity>;
.stream();
}
async getAllForUser(
pagination: PaginationOptions,
userId: string,
options?: PersonSearchOptions,
): Paginated<PersonEntity> {
const items = (await this.db
async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) {
const items = await this.db
.selectFrom('person')
.selectAll('person')
.innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
@@ -198,7 +198,7 @@ export class PersonRepository {
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
.offset(pagination.skip ?? 0)
.limit(pagination.take + 1)
.execute()) as PersonEntity[];
.execute();
if (items.length > pagination.take) {
return { items: items.slice(0, -1), hasNextPage: true };
@@ -208,7 +208,7 @@ export class PersonRepository {
}
@GenerateSql()
getAllWithoutFaces(): Promise<PersonEntity[]> {
getAllWithoutFaces() {
return this.db
.selectFrom('person')
.selectAll('person')
@@ -216,11 +216,11 @@ export class PersonRepository {
.where('asset_faces.deletedAt', 'is', null)
.having((eb) => eb.fn.count('asset_faces.assetId'), '=', 0)
.groupBy('person.id')
.execute() as Promise<PersonEntity[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
getFaces(assetId: string): Promise<AssetFaceEntity[]> {
getFaces(assetId: string) {
return this.db
.selectFrom('asset_faces')
.selectAll('asset_faces')
@@ -228,11 +228,11 @@ export class PersonRepository {
.where('asset_faces.assetId', '=', assetId)
.where('asset_faces.deletedAt', 'is', null)
.orderBy('asset_faces.boundingBoxX1', 'asc')
.execute() as Promise<AssetFaceEntity[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
getFaceById(id: string): Promise<AssetFaceEntity> {
getFaceById(id: string) {
// TODO return null instead of find or fail
return this.db
.selectFrom('asset_faces')
@@ -240,25 +240,57 @@ export class PersonRepository {
.select(withPerson)
.where('asset_faces.id', '=', id)
.where('asset_faces.deletedAt', 'is', null)
.executeTakeFirstOrThrow() as Promise<AssetFaceEntity>;
.executeTakeFirstOrThrow();
}
@GenerateSql({ params: [DummyValue.UUID] })
getFaceByIdWithAssets(
id: string,
relations?: { faceSearch?: boolean },
select?: SelectFaceOptions,
): Promise<AssetFaceEntity | undefined> {
getFaceForFacialRecognitionJob(id: string) {
return this.db
.selectFrom('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))
.select(['asset_faces.id', 'asset_faces.personId', 'asset_faces.sourceType'])
.select((eb) =>
jsonObjectFrom(
eb
.selectFrom('assets')
.select(['assets.ownerId', 'assets.isArchived', 'assets.fileCreatedAt'])
.whereRef('assets.id', '=', 'asset_faces.assetId'),
).as('asset'),
)
.select(withFaceSearch)
.where('asset_faces.id', '=', id)
.where('asset_faces.deletedAt', 'is', null)
.executeTakeFirst() as Promise<AssetFaceEntity | undefined>;
.executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID] })
getDataForThumbnailGenerationJob(id: string) {
return this.db
.selectFrom('person')
.innerJoin('asset_faces', 'asset_faces.id', 'person.faceAssetId')
.innerJoin('assets', 'asset_faces.assetId', 'assets.id')
.innerJoin('exif', 'exif.assetId', 'assets.id')
.innerJoin('asset_files', 'asset_files.assetId', 'assets.id')
.select([
'person.ownerId',
'asset_faces.boundingBoxX1 as x1',
'asset_faces.boundingBoxY1 as y1',
'asset_faces.boundingBoxX2 as x2',
'asset_faces.boundingBoxY2 as y2',
'asset_faces.imageWidth as oldWidth',
'asset_faces.imageHeight as oldHeight',
'exif.exifImageWidth',
'exif.exifImageHeight',
'assets.type',
'assets.originalPath',
'asset_files.path as previewPath',
])
.where('person.id', '=', id)
.where('asset_faces.deletedAt', 'is', null)
.where('asset_files.type', '=', AssetFileType.PREVIEW)
.where('exif.exifImageWidth', '>', 0)
.where('exif.exifImageHeight', '>', 0)
.$narrowType<{ exifImageWidth: NotNull; exifImageHeight: NotNull }>()
.executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
@@ -272,16 +304,16 @@ export class PersonRepository {
return Number(result.numChangedRows ?? 0);
}
getById(personId: string): Promise<PersonEntity | null> {
return (this.db //
getById(personId: string) {
return this.db //
.selectFrom('person')
.selectAll('person')
.where('person.id', '=', personId)
.executeTakeFirst() ?? null) as Promise<PersonEntity | null>;
.executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise<PersonEntity[]> {
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions) {
return this.db
.selectFrom('person')
.selectAll('person')
@@ -296,7 +328,7 @@ export class PersonRepository {
)
.limit(1000)
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
.execute() as Promise<PersonEntity[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] })
@@ -362,8 +394,8 @@ export class PersonRepository {
};
}
create(person: Insertable<Person>): Promise<PersonEntity> {
return this.db.insertInto('person').values(person).returningAll().executeTakeFirst() as Promise<PersonEntity>;
create(person: Insertable<Person>) {
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
}
async createAll(people: Insertable<Person>[]): Promise<string[]> {
@@ -399,13 +431,13 @@ export class PersonRepository {
await query.selectFrom(sql`(select 1)`.as('dummy')).execute();
}
async update(person: Partial<PersonEntity> & { id: string }): Promise<PersonEntity> {
async update(person: Updateable<Person> & { id: string }) {
return this.db
.updateTable('person')
.set(person)
.where('person.id', '=', person.id)
.returningAll()
.executeTakeFirstOrThrow() as Promise<PersonEntity>;
.executeTakeFirstOrThrow();
}
async updateAll(people: Insertable<Person>[]): Promise<void> {
@@ -437,7 +469,7 @@ export class PersonRepository {
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
@ChunkedArray()
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
getFacesByIds(ids: AssetFaceId[]) {
if (ids.length === 0) {
return Promise.resolve([]);
}
@@ -457,17 +489,17 @@ export class PersonRepository {
.where('asset_faces.assetId', 'in', assetIds)
.where('asset_faces.personId', 'in', personIds)
.where('asset_faces.deletedAt', 'is', null)
.execute() as Promise<AssetFaceEntity[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
getRandomFace(personId: string): Promise<AssetFaceEntity | undefined> {
getRandomFace(personId: string) {
return this.db
.selectFrom('asset_faces')
.selectAll('asset_faces')
.where('asset_faces.personId', '=', personId)
.where('asset_faces.deletedAt', 'is', null)
.executeTakeFirst() as Promise<AssetFaceEntity | undefined>;
.executeTakeFirst();
}
@GenerateSql()

View File

@@ -162,7 +162,7 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
hasPerson?: boolean;
numResults: number;
maxDistance: number;
minBirthDate?: Date;
minBirthDate?: Date | null;
}
export interface AssetDuplicateSearch {