refactor: remove album entity, update types (#17450)

This commit is contained in:
Daniel Dietzler
2025-04-18 23:10:34 +02:00
committed by GitHub
parent 854ea13d6a
commit 52ae06c119
44 changed files with 473 additions and 396 deletions

View File

@@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common';
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely';
import { isEmpty, isUndefined, omitBy } from 'lodash';
import { InjectKysely } from 'nestjs-kysely';
import { Stack } from 'src/database';
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { MapAsset } from 'src/dtos/asset-response.dto';
import {
AssetEntity,
hasPeople,
@@ -23,7 +25,7 @@ import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database';
import { globToSqlPattern } from 'src/utils/misc';
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
import { PaginationOptions, paginationHelper } from 'src/utils/pagination';
export type AssetStats = Record<AssetType, number>;
@@ -141,12 +143,12 @@ export interface GetByIdsRelations {
export interface DuplicateGroup {
duplicateId: string;
assets: AssetEntity[];
assets: MapAsset[];
}
export interface DayOfYearAssets {
yearsAgo: number;
assets: AssetEntity[];
assets: MapAsset[];
}
@Injectable()
@@ -234,12 +236,12 @@ export class AssetRepository {
.execute();
}
create(asset: Insertable<Assets>): Promise<AssetEntity> {
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
create(asset: Insertable<Assets>) {
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirstOrThrow();
}
createAll(assets: Insertable<Assets>[]): Promise<AssetEntity[]> {
return this.db.insertInto('assets').values(assets).returningAll().execute() as any as Promise<AssetEntity[]>;
createAll(assets: Insertable<Assets>[]) {
return this.db.insertInto('assets').values(assets).returningAll().execute();
}
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
@@ -299,20 +301,13 @@ export class AssetRepository {
@GenerateSql({ params: [[DummyValue.UUID]] })
@ChunkedArray()
getByIds(ids: string[]): Promise<AssetEntity[]> {
return (
this.db
//
.selectFrom('assets')
.selectAll('assets')
.where('assets.id', '=', anyUuid(ids))
.execute() as Promise<AssetEntity[]>
);
getByIds(ids: string[]) {
return this.db.selectFrom('assets').selectAll('assets').where('assets.id', '=', anyUuid(ids)).execute();
}
@GenerateSql({ params: [[DummyValue.UUID]] })
@ChunkedArray()
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]> {
getByIdsWithAllRelationsButStacks(ids: string[]) {
return this.db
.selectFrom('assets')
.selectAll('assets')
@@ -320,23 +315,8 @@ export class AssetRepository {
.select(withTags)
.$call(withExif)
.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[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
@@ -356,36 +336,29 @@ export class AssetRepository {
return assets.map((asset) => asset.deviceAssetId);
}
getByUserId(
pagination: PaginationOptions,
userId: string,
options: Omit<AssetSearchOptions, 'userIds'> = {},
): Paginated<AssetEntity> {
getByUserId(pagination: PaginationOptions, userId: string, options: Omit<AssetSearchOptions, 'userIds'> = {}) {
return this.getAll(pagination, { ...options, userIds: [userId] });
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | undefined> {
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) {
return this.db
.selectFrom('assets')
.selectAll('assets')
.where('libraryId', '=', asUuid(libraryId))
.where('originalPath', '=', originalPath)
.limit(1)
.executeTakeFirst() as any as Promise<AssetEntity | undefined>;
.executeTakeFirst();
}
async getAll(
pagination: PaginationOptions,
{ orderDirection, ...options }: AssetSearchOptions = {},
): Paginated<AssetEntity> {
async getAll(pagination: PaginationOptions, { orderDirection, ...options }: AssetSearchOptions = {}) {
const builder = searchAssetBuilder(this.db, options)
.select(withFiles)
.orderBy('assets.createdAt', orderDirection ?? 'asc')
.limit(pagination.take + 1)
.offset(pagination.skip ?? 0);
const items = await builder.execute();
return paginationHelper(items as any as AssetEntity[], pagination.take);
return paginationHelper(items, pagination.take);
}
/**
@@ -420,23 +393,22 @@ export class AssetRepository {
}
@GenerateSql({ params: [DummyValue.UUID] })
getById(
id: string,
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {},
): Promise<AssetEntity | undefined> {
getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) {
return this.db
.selectFrom('assets')
.selectAll('assets')
.where('assets.id', '=', asUuid(id))
.$if(!!exifInfo, withExif)
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces))
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>())
.$if(!!library, (qb) => qb.select(withLibrary))
.$if(!!owner, (qb) => qb.select(withOwner))
.$if(!!smartSearch, withSmartSearch)
.$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.select((eb) => eb.fn.toJson(eb.table('asset_stack')).$castTo<Stack | null>().as('stack')),
)
.$if(!!stack!.assets, (qb) =>
qb
.leftJoinLateral(
@@ -453,13 +425,13 @@ export class AssetRepository {
.as('stacked_assets'),
(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')).$castTo<Stack | null>().as('stack')),
),
)
.$if(!!files, (qb) => qb.select(withFiles))
.$if(!!tags, (qb) => qb.select(withTags))
.limit(1)
.executeTakeFirst() as any as Promise<AssetEntity | undefined>;
.executeTakeFirst();
}
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
@@ -488,7 +460,7 @@ export class AssetRepository {
.execute();
}
async update(asset: Updateable<Assets> & { id: string }): Promise<AssetEntity> {
async update(asset: Updateable<Assets> & { id: string }) {
const value = omitBy(asset, isUndefined);
delete value.id;
if (!isEmpty(value)) {
@@ -498,10 +470,10 @@ export class AssetRepository {
.selectAll('assets')
.$call(withExif)
.$call((qb) => qb.select(withFacesAndPeople))
.executeTakeFirst() as Promise<AssetEntity>;
.executeTakeFirst();
}
return this.getById(asset.id, { exifInfo: true, faces: { person: true } }) as Promise<AssetEntity>;
return this.getById(asset.id, { exifInfo: true, faces: { person: true } });
}
async remove(asset: { id: string }): Promise<void> {
@@ -509,7 +481,7 @@ export class AssetRepository {
}
@GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] })
getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions): Promise<AssetEntity | undefined> {
getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions) {
return this.db
.selectFrom('assets')
.selectAll('assets')
@@ -517,7 +489,7 @@ export class AssetRepository {
.where('checksum', '=', checksum)
.$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null)))
.limit(1)
.executeTakeFirst() as Promise<AssetEntity | undefined>;
.executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] })
@@ -544,7 +516,7 @@ export class AssetRepository {
return asset?.id;
}
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | undefined> {
findLivePhotoMatch(options: LivePhotoSearchOptions) {
const { ownerId, otherAssetId, livePhotoCID, type } = options;
return this.db
.selectFrom('assets')
@@ -555,7 +527,7 @@ export class AssetRepository {
.where('type', '=', type)
.where('exif.livePhotoCID', '=', livePhotoCID)
.limit(1)
.executeTakeFirst() as Promise<AssetEntity | undefined>;
.executeTakeFirst();
}
@GenerateSql(
@@ -564,7 +536,7 @@ export class AssetRepository {
params: [DummyValue.PAGINATION, property],
})),
)
async getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
async getWithout(pagination: PaginationOptions, property: WithoutProperty) {
const items = await this.db
.selectFrom('assets')
.selectAll('assets')
@@ -626,7 +598,7 @@ export class AssetRepository {
.orderBy('createdAt')
.execute();
return paginationHelper(items as any as AssetEntity[], pagination.take);
return paginationHelper(items, pagination.take);
}
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
@@ -645,7 +617,7 @@ export class AssetRepository {
.executeTakeFirstOrThrow();
}
getRandom(userIds: string[], take: number): Promise<AssetEntity[]> {
getRandom(userIds: string[], take: number) {
return this.db
.selectFrom('assets')
.selectAll('assets')
@@ -655,7 +627,7 @@ export class AssetRepository {
.where('deletedAt', 'is', null)
.orderBy((eb) => eb.fn('random'))
.limit(take)
.execute() as any as Promise<AssetEntity[]>;
.execute();
}
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
@@ -708,7 +680,7 @@ export class AssetRepository {
}
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
async getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
return this.db
.selectFrom('assets')
.selectAll('assets')
@@ -741,7 +713,7 @@ export class AssetRepository {
.as('stacked_assets'),
(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').$castTo<Stack | null>()).as('stack')),
)
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
.$if(options.isDuplicate !== undefined, (qb) =>
@@ -753,11 +725,11 @@ export class AssetRepository {
.where('assets.isVisible', '=', true)
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
.orderBy('assets.localDateTime', options.order ?? 'desc')
.execute() as any as Promise<AssetEntity[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
getDuplicates(userId: string): Promise<DuplicateGroup[]> {
getDuplicates(userId: string) {
return (
this.db
.with('duplicates', (qb) =>
@@ -774,9 +746,15 @@ export class AssetRepository {
(join) => join.onTrue(),
)
.select('assets.duplicateId')
.select((eb) => eb.fn('jsonb_agg', [eb.table('asset')]).as('assets'))
.select((eb) =>
eb
.fn('jsonb_agg', [eb.table('asset')])
.$castTo<MapAsset[]>()
.as('assets'),
)
.where('assets.ownerId', '=', asUuid(userId))
.where('assets.duplicateId', 'is not', null)
.$narrowType<{ duplicateId: NotNull }>()
.where('assets.deletedAt', 'is', null)
.where('assets.isVisible', '=', true)
.where('assets.stackId', 'is', null)
@@ -801,7 +779,7 @@ export class AssetRepository {
.where(({ not, exists }) =>
not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))),
)
.execute() as any as Promise<DuplicateGroup[]>
.execute()
);
}
@@ -845,7 +823,7 @@ export class AssetRepository {
},
],
})
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
getAllForUserFullSync(options: AssetFullSyncOptions) {
const { ownerId, lastId, updatedUntil, limit } = options;
return this.db
.selectFrom('assets')
@@ -863,18 +841,18 @@ export class AssetRepository {
.as('stacked_assets'),
(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')).$castTo<Stack | null>().as('stack'))
.where('assets.ownerId', '=', asUuid(ownerId))
.where('assets.isVisible', '=', true)
.where('assets.updatedAt', '<=', updatedUntil)
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
.orderBy('assets.id')
.limit(limit)
.execute() as any as Promise<AssetEntity[]>;
.execute();
}
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] })
async getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
async getChangedDeltaSync(options: AssetDeltaSyncOptions) {
return this.db
.selectFrom('assets')
.selectAll('assets')
@@ -891,12 +869,12 @@ export class AssetRepository {
.as('stacked_assets'),
(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').$castTo<Stack | null>()).as('stack'))
.where('assets.ownerId', '=', anyUuid(options.userIds))
.where('assets.isVisible', '=', true)
.where('assets.updatedAt', '>', options.updatedAfter)
.limit(options.limit)
.execute() as any as Promise<AssetEntity[]>;
.execute();
}
async upsertFile(file: Pick<Insertable<AssetFiles>, 'assetId' | 'path' | 'type'>): Promise<void> {