mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: favorite albums
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { DB } from 'src/schema';
|
||||
import { AlbumUserMetadataTable } from 'src/schema/tables/album-user-metadata.table';
|
||||
|
||||
export type AlbumUserMetadataId = {
|
||||
albumId: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AlbumUserMetadataRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ albumId: DummyValue.UUID, userId: DummyValue.UUID, isFavorite: true }] })
|
||||
async upsert(dto: Insertable<AlbumUserMetadataTable>) {
|
||||
await this.db
|
||||
.insertInto('album_user_metadata')
|
||||
.values(dto)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['albumId', 'userId']).doUpdateSet((eb) => ({ isFavorite: eb.ref('excluded.isFavorite') })),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,21 @@ export class AlbumUserRepository {
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
|
||||
create(albumUser: Insertable<AlbumUserTable>) {
|
||||
return this.db
|
||||
.insertInto('album_user')
|
||||
.values(albumUser)
|
||||
.returning(['userId', 'albumId', 'role'])
|
||||
.executeTakeFirstOrThrow();
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const result = await tx
|
||||
.insertInto('album_user')
|
||||
.values(albumUser)
|
||||
.returning(['userId', 'albumId', 'role'])
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await tx
|
||||
.insertInto('album_user_metadata')
|
||||
.values({ albumId: albumUser.albumId, userId: albumUser.userId, isFavorite: false })
|
||||
.onConflict((oc) => oc.columns(['albumId', 'userId']).doNothing())
|
||||
.execute();
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }, { role: AlbumUserRole.Viewer }] })
|
||||
@@ -36,6 +46,9 @@ export class AlbumUserRepository {
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
|
||||
async delete({ userId, albumId }: AlbumPermissionId): Promise<void> {
|
||||
await this.db.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute();
|
||||
await this.db.transaction().execute(async (tx) => {
|
||||
await tx.deleteFrom('album_user_metadata').where('userId', '=', userId).where('albumId', '=', albumId).execute();
|
||||
await tx.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,18 @@ export interface AlbumInfoOptions {
|
||||
withAssets: boolean;
|
||||
}
|
||||
|
||||
const withFavorite = (eb: ExpressionBuilder<DB, 'album'>, userId?: string) => {
|
||||
if (!userId) {
|
||||
return sql<boolean>`false`.as('isFavorite');
|
||||
}
|
||||
|
||||
return sql<boolean>`coalesce(${eb
|
||||
.selectFrom('album_user_metadata')
|
||||
.select('album_user_metadata.isFavorite')
|
||||
.whereRef('album_user_metadata.albumId', '=', 'album.id')
|
||||
.where('album_user_metadata.userId', '=', userId)}, false)`.as('isFavorite');
|
||||
};
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('user').select(columns.user).whereRef('user.id', '=', 'album.ownerId'))
|
||||
.$notNull()
|
||||
@@ -84,14 +96,15 @@ const withAssets = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
export class AlbumRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
||||
async getById(id: string, options: AlbumInfoOptions) {
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }, DummyValue.UUID] })
|
||||
async getById(id: string, options: AlbumInfoOptions, userId?: string) {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.where('album.id', '=', id)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.select(withOwner)
|
||||
.select((eb) => withFavorite(eb, userId))
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||
@@ -119,6 +132,7 @@ export class AlbumRepository {
|
||||
.where('album_asset.assetId', '=', assetId)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.select((eb) => withFavorite(eb, ownerId))
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
@@ -194,6 +208,7 @@ export class AlbumRepository {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.select((eb) => withFavorite(eb, ownerId))
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
@@ -211,6 +226,7 @@ export class AlbumRepository {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.select((eb) => withFavorite(eb, ownerId))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.exists(
|
||||
@@ -243,6 +259,7 @@ export class AlbumRepository {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.select((eb) => withFavorite(eb, ownerId))
|
||||
.where('album.ownerId', '=', ownerId)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.where((eb) => eb.not(eb.exists(eb.selectFrom('album_user').whereRef('album_user.albumId', '=', 'album.id'))))
|
||||
@@ -318,6 +335,15 @@ export class AlbumRepository {
|
||||
throw new Error('Failed to create album');
|
||||
}
|
||||
|
||||
await tx
|
||||
.insertInto('album_user_metadata')
|
||||
.values([
|
||||
{ albumId: newAlbum.id, userId: album.ownerId, isFavorite: false },
|
||||
...albumUsers.map((albumUser) => ({ albumId: newAlbum.id, userId: albumUser.userId, isFavorite: false })),
|
||||
])
|
||||
.onConflict((oc) => oc.columns(['albumId', 'userId']).doNothing())
|
||||
.execute();
|
||||
|
||||
if (assetIds.length > 0) {
|
||||
await this.addAssets(tx, newAlbum.id, assetIds);
|
||||
}
|
||||
@@ -335,6 +361,7 @@ export class AlbumRepository {
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.where('id', '=', newAlbum.id)
|
||||
.select((eb) => withFavorite(eb, album.ownerId))
|
||||
.select(withOwner)
|
||||
.select(withAssets)
|
||||
.select(withAlbumUsers)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserMetadataRepository } from 'src/repositories/album-user-metadata.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
@@ -55,6 +56,7 @@ export const repositories = [
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AlbumRepository,
|
||||
AlbumUserMetadataRepository,
|
||||
AlbumUserRepository,
|
||||
AuditRepository,
|
||||
ApiKeyRepository,
|
||||
|
||||
@@ -49,6 +49,7 @@ export class SyncRepository {
|
||||
album: AlbumSync;
|
||||
albumAsset: AlbumAssetSync;
|
||||
albumAssetExif: AlbumAssetExifSync;
|
||||
albumUserMetadata: AlbumUserMetadataSync;
|
||||
albumToAsset: AlbumToAssetSync;
|
||||
albumUser: AlbumUserSync;
|
||||
asset: AssetSync;
|
||||
@@ -72,6 +73,7 @@ export class SyncRepository {
|
||||
this.album = new AlbumSync(this.db);
|
||||
this.albumAsset = new AlbumAssetSync(this.db);
|
||||
this.albumAssetExif = new AlbumAssetExifSync(this.db);
|
||||
this.albumUserMetadata = new AlbumUserMetadataSync(this.db);
|
||||
this.albumToAsset = new AlbumToAssetSync(this.db);
|
||||
this.albumUser = new AlbumUserSync(this.db);
|
||||
this.asset = new AssetSync(this.db);
|
||||
@@ -385,6 +387,29 @@ class AlbumUserSync extends BaseSync {
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumUserMetadataSync extends BaseSync {
|
||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||
getDeletes(options: SyncQueryOptions) {
|
||||
return this.auditQuery('album_user_metadata_audit', options)
|
||||
.select(['id', 'albumId', 'userId'])
|
||||
.where('userId', '=', options.userId)
|
||||
.stream();
|
||||
}
|
||||
|
||||
cleanupAuditTable(daysAgo: number) {
|
||||
return this.auditCleanup('album_user_metadata_audit', daysAgo);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||
getUpserts(options: SyncQueryOptions) {
|
||||
return this.upsertQuery('album_user_metadata', options)
|
||||
.select(columns.syncAlbumUserMetadata)
|
||||
.select('album_user_metadata.updateId')
|
||||
.where('userId', '=', options.userId)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AssetSync extends BaseSync {
|
||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||
getDeletes(options: SyncQueryOptions) {
|
||||
|
||||
Reference in New Issue
Block a user