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,12 +1,11 @@
import { Injectable } from '@nestjs/common';
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { columns, Exif } from 'src/database';
import { Albums, DB } from 'src/db';
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
import { AlbumEntity } from 'src/entities/album.entity';
export interface AlbumAssetCount {
albumId: string;
@@ -21,9 +20,9 @@ export interface AlbumInfoOptions {
}
const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => {
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId')).as(
'owner',
);
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId'))
.$notNull()
.as('owner');
};
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
@@ -32,12 +31,14 @@ const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
.selectFrom('albums_shared_users_users as album_users')
.select('album_users.role')
.select((eb) =>
jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId')).as(
'user',
),
jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId'))
.$notNull()
.as('user'),
)
.whereRef('album_users.albumsId', '=', 'albums.id'),
).as('albumUsers');
)
.$notNull()
.as('albumUsers');
};
const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => {
@@ -53,7 +54,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
.selectFrom('assets')
.selectAll('assets')
.leftJoin('exif', 'assets.id', 'exif.assetId')
.select((eb) => eb.table('exif').as('exifInfo'))
.select((eb) => eb.table('exif').$castTo<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)
@@ -69,7 +70,7 @@ export class AlbumRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> {
async getById(id: string, options: AlbumInfoOptions) {
return this.db
.selectFrom('albums')
.selectAll('albums')
@@ -79,11 +80,12 @@ export class AlbumRepository {
.select(withAlbumUsers)
.select(withSharedLink)
.$if(options.withAssets, (eb) => eb.select(withAssets))
.executeTakeFirst() as Promise<AlbumEntity | undefined>;
.$narrowType<{ assets: NotNull }>()
.executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
async getByAssetId(ownerId: string, assetId: string) {
return this.db
.selectFrom('albums')
.selectAll('albums')
@@ -105,7 +107,7 @@ export class AlbumRepository {
.select(withOwner)
.select(withAlbumUsers)
.orderBy('albums.createdAt', 'desc')
.execute() as unknown as Promise<AlbumEntity[]>;
.execute();
}
@GenerateSql({ params: [[DummyValue.UUID]] })
@@ -134,7 +136,7 @@ export class AlbumRepository {
}
@GenerateSql({ params: [DummyValue.UUID] })
async getOwned(ownerId: string): Promise<AlbumEntity[]> {
async getOwned(ownerId: string) {
return this.db
.selectFrom('albums')
.selectAll('albums')
@@ -144,14 +146,14 @@ export class AlbumRepository {
.where('albums.ownerId', '=', ownerId)
.where('albums.deletedAt', 'is', null)
.orderBy('albums.createdAt', 'desc')
.execute() as unknown as Promise<AlbumEntity[]>;
.execute();
}
/**
* Get albums shared with and shared by owner.
*/
@GenerateSql({ params: [DummyValue.UUID] })
async getShared(ownerId: string): Promise<AlbumEntity[]> {
async getShared(ownerId: string) {
return this.db
.selectFrom('albums')
.selectAll('albums')
@@ -176,14 +178,14 @@ export class AlbumRepository {
.select(withOwner)
.select(withSharedLink)
.orderBy('albums.createdAt', 'desc')
.execute() as unknown as Promise<AlbumEntity[]>;
.execute();
}
/**
* Get albums of owner that are _not_ shared
*/
@GenerateSql({ params: [DummyValue.UUID] })
async getNotShared(ownerId: string): Promise<AlbumEntity[]> {
async getNotShared(ownerId: string) {
return this.db
.selectFrom('albums')
.selectAll('albums')
@@ -203,7 +205,7 @@ export class AlbumRepository {
)
.select(withOwner)
.orderBy('albums.createdAt', 'desc')
.execute() as unknown as Promise<AlbumEntity[]>;
.execute();
}
async restoreAll(userId: string): Promise<void> {
@@ -262,7 +264,7 @@ export class AlbumRepository {
await this.addAssets(this.db, albumId, assetIds);
}
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity> {
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]) {
return this.db.transaction().execute(async (tx) => {
const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst();
@@ -290,11 +292,12 @@ export class AlbumRepository {
.select(withOwner)
.select(withAssets)
.select(withAlbumUsers)
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
.$narrowType<{ assets: NotNull }>()
.executeTakeFirstOrThrow();
});
}
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> {
update(id: string, album: Updateable<Albums>) {
return this.db
.updateTable('albums')
.set(album)
@@ -303,7 +306,7 @@ export class AlbumRepository {
.returning(withOwner)
.returning(withSharedLink)
.returning(withAlbumUsers)
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
.executeTakeFirstOrThrow();
}
async delete(id: string): Promise<void> {

View File

@@ -8,7 +8,7 @@ import { DummyValue, GenerateSql } from 'src/decorators';
import { withExifInner, withFaces, withFiles } from 'src/entities/asset.entity';
import { AssetFileType } from 'src/enum';
import { StorageAsset } from 'src/types';
import { asUuid } from 'src/utils/database';
import { anyUuid, asUuid } from 'src/utils/database';
@Injectable()
export class AssetJobRepository {
@@ -149,6 +149,21 @@ export class AssetJobRepository {
.executeTakeFirst();
}
getForSyncAssets(ids: string[]) {
return this.db
.selectFrom('assets')
.select([
'assets.id',
'assets.isOffline',
'assets.libraryId',
'assets.originalPath',
'assets.status',
'assets.fileModifiedAt',
])
.where('assets.id', '=', anyUuid(ids))
.execute();
}
private storageTemplateAssetQuery() {
return this.db
.selectFrom('assets')

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> {

View File

@@ -1,13 +1,13 @@
import { Injectable } from '@nestjs/common';
import { Kysely, OrderByDirection, sql } from 'kysely';
import { Kysely, OrderByDirection, Selectable, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { randomUUID } from 'node:crypto';
import { DB } from 'src/db';
import { DB, Exif } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { searchAssetBuilder } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum';
import { anyUuid, asUuid } from 'src/utils/database';
import { Paginated } from 'src/utils/pagination';
import { isValidInteger } from 'src/validation';
export interface SearchResult<T> {
@@ -216,7 +216,7 @@ export class SearchRepository {
},
],
})
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
const items = await searchAssetBuilder(this.db, options)
.orderBy('assets.fileCreatedAt', orderDirection)
@@ -225,7 +225,7 @@ export class SearchRepository {
.execute();
const hasNextPage = items.length > pagination.size;
items.splice(pagination.size);
return { items: items as any as AssetEntity[], hasNextPage };
return { items, hasNextPage };
}
@GenerateSql({
@@ -240,7 +240,7 @@ export class SearchRepository {
},
],
})
async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
async searchRandom(size: number, options: AssetSearchOptions) {
const uuid = randomUUID();
const builder = searchAssetBuilder(this.db, options);
const lessThan = builder
@@ -251,8 +251,8 @@ export class SearchRepository {
.where('assets.id', '>', uuid)
.orderBy(sql`random()`)
.limit(size);
const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
return rows as any as AssetEntity[];
const { rows } = await sql<MapAsset>`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
return rows;
}
@GenerateSql({
@@ -268,17 +268,17 @@ export class SearchRepository {
},
],
})
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity> {
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) {
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
throw new Error(`Invalid value for 'size': ${pagination.size}`);
}
const items = (await searchAssetBuilder(this.db, options)
const items = await searchAssetBuilder(this.db, options)
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
.limit(pagination.size + 1)
.offset((pagination.page - 1) * pagination.size)
.execute()) as any as AssetEntity[];
.execute();
const hasNextPage = items.length > pagination.size;
items.splice(pagination.size);
@@ -392,7 +392,7 @@ export class SearchRepository {
}
@GenerateSql({ params: [[DummyValue.UUID]] })
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
getAssetsByCity(userIds: string[]) {
return this.db
.withRecursive('cte', (qb) => {
const base = qb
@@ -434,9 +434,14 @@ export class SearchRepository {
.innerJoin('exif', 'assets.id', 'exif.assetId')
.innerJoin('cte', 'assets.id', 'cte.assetId')
.selectAll('assets')
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'))
.select((eb) =>
eb
.fn('to_jsonb', [eb.table('exif')])
.$castTo<Selectable<Exif>>()
.as('exifInfo'),
)
.orderBy('exif.city')
.execute() as any as Promise<AssetEntity[]>;
.execute();
}
async upsert(assetId: string, embedding: string): Promise<void> {

View File

@@ -1,12 +1,12 @@
import { Injectable } from '@nestjs/common';
import { Insertable, Kysely, sql, Updateable } from 'kysely';
import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import _ from 'lodash';
import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { Album, columns } from 'src/database';
import { DB, SharedLinks } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { SharedLinkType } from 'src/enum';
export type SharedLinkSearchOptions = {
@@ -19,7 +19,7 @@ export class SharedLinkRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
get(userId: string, id: string): Promise<SharedLinkEntity | undefined> {
get(userId: string, id: string) {
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
@@ -87,18 +87,23 @@ export class SharedLinkRepository {
.as('album'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`).as('assets'))
.select((eb) =>
eb.fn
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
.$castTo<MapAsset[]>()
.as('assets'),
)
.groupBy(['shared_links.id', sql`"album".*`])
.select((eb) => eb.fn.toJson('album').as('album'))
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
.where('shared_links.id', '=', id)
.where('shared_links.userId', '=', userId)
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
.orderBy('shared_links.createdAt', 'desc')
.executeTakeFirst() as Promise<SharedLinkEntity | undefined>;
.executeTakeFirst();
}
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
getAll({ userId, albumId }: SharedLinkSearchOptions): Promise<SharedLinkEntity[]> {
getAll({ userId, albumId }: SharedLinkSearchOptions) {
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
@@ -115,6 +120,7 @@ export class SharedLinkRepository {
(join) => join.onTrue(),
)
.select('assets.assets')
.$narrowType<{ assets: NotNull }>()
.leftJoinLateral(
(eb) =>
eb
@@ -152,12 +158,12 @@ export class SharedLinkRepository {
.as('album'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('album').as('album'))
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
.$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!))
.orderBy('shared_links.createdAt', 'desc')
.distinctOn(['shared_links.createdAt'])
.execute() as unknown as Promise<SharedLinkEntity[]>;
.execute();
}
@GenerateSql({ params: [DummyValue.BUFFER] })
@@ -177,7 +183,7 @@ export class SharedLinkRepository {
.executeTakeFirst();
}
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity> {
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }) {
const { id } = await this.db
.insertInto('shared_links')
.values(_.omit(entity, 'assetIds'))
@@ -194,7 +200,7 @@ export class SharedLinkRepository {
return this.getSharedLinks(id);
}
async update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }): Promise<SharedLinkEntity> {
async update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }) {
const { id } = await this.db
.updateTable('shared_links')
.set(_.omit(entity, 'assets', 'album', 'assetIds'))
@@ -212,8 +218,8 @@ export class SharedLinkRepository {
return this.getSharedLinks(id);
}
async remove(entity: SharedLinkEntity): Promise<void> {
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute();
async remove(id: string): Promise<void> {
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', id).execute();
}
private getSharedLinks(id: string) {
@@ -236,9 +242,12 @@ export class SharedLinkRepository {
(join) => join.onTrue(),
)
.select((eb) =>
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'),
eb.fn
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
.$castTo<MapAsset[]>()
.as('assets'),
)
.groupBy('shared_links.id')
.executeTakeFirstOrThrow() as Promise<SharedLinkEntity>;
.executeTakeFirstOrThrow();
}
}

View File

@@ -5,7 +5,6 @@ import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { AssetStack, DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { asUuid } from 'src/utils/database';
export interface StackSearch {
@@ -36,9 +35,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
.where('assets.deletedAt', 'is', null)
.whereRef('assets.stackId', '=', 'asset_stack.id'),
)
.$castTo<AssetEntity[]>()
.as('assets');
).as('assets');
};
@Injectable()