mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
infra(server): fix Album TypeORM relations and change ids to uuids (#1582)
* infra: make api-key primary key column a UUID * infra: move ManyToMany relations in album entity, make ownerId ManyToOne --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
917f1dea9f
commit
000d0a08f4
24 changed files with 368 additions and 461 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { AlbumEntity, AssetAlbumEntity, UserAlbumEntity } from '@app/infra';
|
||||
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository, SelectQueryBuilder, DataSource, Brackets, Not, IsNull } from 'typeorm';
|
||||
import { Repository, Not, IsNull, FindManyOptions } from 'typeorm';
|
||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||
import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
|
|
@ -15,7 +15,7 @@ export interface IAlbumRepository {
|
|||
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
||||
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>;
|
||||
getPublicSharingList(ownerId: string): Promise<AlbumEntity[]>;
|
||||
get(albumId: string): Promise<AlbumEntity | undefined>;
|
||||
get(albumId: string): Promise<AlbumEntity | null>;
|
||||
delete(album: AlbumEntity): Promise<void>;
|
||||
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
||||
removeUser(album: AlbumEntity, userId: string): Promise<void>;
|
||||
|
|
@ -34,14 +34,6 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
constructor(
|
||||
@InjectRepository(AlbumEntity)
|
||||
private albumRepository: Repository<AlbumEntity>,
|
||||
|
||||
@InjectRepository(AssetAlbumEntity)
|
||||
private assetAlbumRepository: Repository<AssetAlbumEntity>,
|
||||
|
||||
@InjectRepository(UserAlbumEntity)
|
||||
private userAlbumRepository: Repository<UserAlbumEntity>,
|
||||
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async getPublicSharingList(ownerId: string): Promise<AlbumEntity[]> {
|
||||
|
|
@ -62,194 +54,98 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
|
||||
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
|
||||
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
|
||||
|
||||
const sharedAlbums = await this.userAlbumRepository.count({
|
||||
where: { sharedUserId: userId },
|
||||
});
|
||||
|
||||
let sharedAlbumCount = 0;
|
||||
ownedAlbums.map((album) => {
|
||||
if (album.sharedUsers?.length) {
|
||||
sharedAlbumCount += 1;
|
||||
}
|
||||
});
|
||||
const sharedAlbums = await this.albumRepository.count({ where: { sharedUsers: { id: userId } } });
|
||||
const sharedAlbumCount = ownedAlbums.filter((album) => album.sharedUsers?.length > 0).length;
|
||||
|
||||
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharedAlbumCount);
|
||||
}
|
||||
|
||||
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
|
||||
return this.dataSource.transaction(async (transactionalEntityManager) => {
|
||||
// Create album entity
|
||||
const newAlbum = new AlbumEntity();
|
||||
newAlbum.ownerId = ownerId;
|
||||
newAlbum.albumName = createAlbumDto.albumName;
|
||||
|
||||
let album = await transactionalEntityManager.save(newAlbum);
|
||||
album = await transactionalEntityManager.findOneOrFail(AlbumEntity, {
|
||||
where: { id: album.id },
|
||||
relations: ['owner'],
|
||||
});
|
||||
|
||||
// Add shared users
|
||||
if (createAlbumDto.sharedWithUserIds?.length) {
|
||||
for (const sharedUserId of createAlbumDto.sharedWithUserIds) {
|
||||
const newSharedUser = new UserAlbumEntity();
|
||||
newSharedUser.albumId = album.id;
|
||||
newSharedUser.sharedUserId = sharedUserId;
|
||||
|
||||
await transactionalEntityManager.save(newSharedUser);
|
||||
}
|
||||
}
|
||||
|
||||
// Add shared assets
|
||||
const newRecords: AssetAlbumEntity[] = [];
|
||||
|
||||
if (createAlbumDto.assetIds?.length) {
|
||||
for (const assetId of createAlbumDto.assetIds) {
|
||||
const newAssetAlbum = new AssetAlbumEntity();
|
||||
newAssetAlbum.assetId = assetId;
|
||||
newAssetAlbum.albumId = album.id;
|
||||
|
||||
newRecords.push(newAssetAlbum);
|
||||
}
|
||||
}
|
||||
|
||||
if (!album.albumThumbnailAssetId && newRecords.length > 0) {
|
||||
album.albumThumbnailAssetId = newRecords[0].assetId;
|
||||
await transactionalEntityManager.save(album);
|
||||
}
|
||||
|
||||
await transactionalEntityManager.save([...newRecords]);
|
||||
|
||||
return album;
|
||||
async create(ownerId: string, dto: CreateAlbumDto): Promise<AlbumEntity> {
|
||||
const album = await this.albumRepository.save({
|
||||
ownerId,
|
||||
albumName: dto.albumName,
|
||||
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
|
||||
assets: dto.assetIds?.map((value) => ({ id: value } as AssetEntity)) ?? [],
|
||||
albumThumbnailAssetId: dto.assetIds?.[0] || null,
|
||||
});
|
||||
|
||||
// need to re-load the relations
|
||||
return this.get(album.id) as Promise<AlbumEntity>;
|
||||
}
|
||||
|
||||
async getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
||||
const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
|
||||
const userId = ownerId;
|
||||
let query = this.albumRepository.createQueryBuilder('album');
|
||||
|
||||
const getSharedAlbumIdsSubQuery = (qb: SelectQueryBuilder<AlbumEntity>) => {
|
||||
return qb
|
||||
.subQuery()
|
||||
.select('albumSub.id')
|
||||
.from(AlbumEntity, 'albumSub')
|
||||
.innerJoin('albumSub.sharedUsers', 'userAlbumSub')
|
||||
.where('albumSub.ownerId = :ownerId', { ownerId: userId })
|
||||
.getQuery();
|
||||
const queryProperties: FindManyOptions<AlbumEntity> = {
|
||||
relations: { sharedUsers: true, assets: true, sharedLinks: true, owner: true },
|
||||
order: { assets: { createdAt: 'ASC' }, createdAt: 'ASC' },
|
||||
};
|
||||
|
||||
let albumsQuery: Promise<AlbumEntity[]>;
|
||||
|
||||
/**
|
||||
* `shared` boolean usage
|
||||
* true = shared with me, and my albums that are shared
|
||||
* false = my albums that are not shared
|
||||
* undefined = all my albums
|
||||
*/
|
||||
if (filteringByShared) {
|
||||
if (getAlbumsDto.shared) {
|
||||
// shared albums
|
||||
query = query
|
||||
.innerJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||
.innerJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||
.where((qb) => {
|
||||
// owned and shared with other users
|
||||
const subQuery = getSharedAlbumIdsSubQuery(qb);
|
||||
return `album.id IN ${subQuery}`;
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
// shared with userId
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('userAlbum.albumId')
|
||||
.from(UserAlbumEntity, 'userAlbum')
|
||||
.where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
|
||||
.getQuery();
|
||||
return `album.id IN ${subQuery}`;
|
||||
});
|
||||
albumsQuery = this.albumRepository.find({
|
||||
where: [{ sharedUsers: { id: userId } }, { ownerId: userId, sharedUsers: { id: Not(IsNull()) } }],
|
||||
...queryProperties,
|
||||
});
|
||||
} else {
|
||||
// owned, not shared albums
|
||||
query = query.where('album.ownerId = :ownerId', { ownerId: userId }).andWhere((qb) => {
|
||||
const subQuery = getSharedAlbumIdsSubQuery(qb);
|
||||
return `album.id NOT IN ${subQuery}`;
|
||||
albumsQuery = this.albumRepository.find({
|
||||
where: { ownerId: userId, sharedUsers: { id: IsNull() } },
|
||||
...queryProperties,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// owned and shared with userId
|
||||
query = query
|
||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||
.where('album.ownerId = :ownerId', { ownerId: userId });
|
||||
// owned
|
||||
albumsQuery = this.albumRepository.find({
|
||||
where: { ownerId: userId },
|
||||
...queryProperties,
|
||||
});
|
||||
}
|
||||
|
||||
// Get information of assets in albums
|
||||
query = query
|
||||
.leftJoinAndSelect('album.assets', 'assets')
|
||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
|
||||
|
||||
// Get information of shared links in albums
|
||||
query = query.leftJoinAndSelect('album.sharedLinks', 'sharedLink');
|
||||
|
||||
// get information of owner of albums
|
||||
query = query.leftJoinAndSelect('album.owner', 'owner');
|
||||
|
||||
const albums = await query.getMany();
|
||||
const albums = await albumsQuery;
|
||||
|
||||
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
||||
|
||||
return albums;
|
||||
return albumsQuery;
|
||||
}
|
||||
|
||||
async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
const query = this.albumRepository.createQueryBuilder('album');
|
||||
|
||||
const albums = await query
|
||||
.where('album.ownerId = :ownerId', { ownerId: userId })
|
||||
.andWhere((qb) => {
|
||||
// shared with userId
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('assetAlbum.albumId')
|
||||
.from(AssetAlbumEntity, 'assetAlbum')
|
||||
.where('assetAlbum.assetId = :assetId', { assetId: assetId })
|
||||
.getQuery();
|
||||
return `album.id IN ${subQuery}`;
|
||||
})
|
||||
.leftJoinAndSelect('album.owner', 'owner')
|
||||
.leftJoinAndSelect('album.assets', 'assets')
|
||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
|
||||
.getMany();
|
||||
const albums = await this.albumRepository.find({
|
||||
where: { ownerId: userId, assets: { id: assetId } },
|
||||
relations: { owner: true, assets: true, sharedUsers: true },
|
||||
order: { assets: { createdAt: 'ASC' } },
|
||||
});
|
||||
|
||||
return albums;
|
||||
}
|
||||
|
||||
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
||||
const album = await this.albumRepository.findOne({
|
||||
async get(albumId: string): Promise<AlbumEntity | null> {
|
||||
return this.albumRepository.findOne({
|
||||
where: { id: albumId },
|
||||
relations: {
|
||||
owner: true,
|
||||
sharedUsers: {
|
||||
userInfo: true,
|
||||
},
|
||||
sharedUsers: true,
|
||||
assets: {
|
||||
assetInfo: {
|
||||
exifInfo: true,
|
||||
},
|
||||
exifInfo: true,
|
||||
},
|
||||
sharedLinks: true,
|
||||
},
|
||||
order: {
|
||||
assets: {
|
||||
assetInfo: {
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!album) {
|
||||
return;
|
||||
}
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
async delete(album: AlbumEntity): Promise<void> {
|
||||
|
|
@ -257,67 +153,53 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
}
|
||||
|
||||
async addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity> {
|
||||
const newRecords: UserAlbumEntity[] = [];
|
||||
album.sharedUsers.push(...addUsersDto.sharedUserIds.map((id) => ({ id } as UserEntity)));
|
||||
|
||||
for (const sharedUserId of addUsersDto.sharedUserIds) {
|
||||
const newEntity = new UserAlbumEntity();
|
||||
newEntity.albumId = album.id;
|
||||
newEntity.sharedUserId = sharedUserId;
|
||||
await this.albumRepository.save(album);
|
||||
|
||||
newRecords.push(newEntity);
|
||||
}
|
||||
|
||||
await this.userAlbumRepository.save([...newRecords]);
|
||||
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
|
||||
|
||||
return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure
|
||||
// need to re-load the shared user relation
|
||||
return this.get(album.id) as Promise<AlbumEntity>;
|
||||
}
|
||||
|
||||
async removeUser(album: AlbumEntity, userId: string): Promise<void> {
|
||||
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId });
|
||||
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
|
||||
album.sharedUsers = album.sharedUsers.filter((user) => user.id !== userId);
|
||||
await this.albumRepository.save(album);
|
||||
}
|
||||
|
||||
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
|
||||
const res = await this.assetAlbumRepository.delete({
|
||||
albumId: album.id,
|
||||
assetId: In(removeAssetsDto.assetIds),
|
||||
const assetCount = album.assets.length;
|
||||
|
||||
album.assets = album.assets.filter((asset) => {
|
||||
return !removeAssetsDto.assetIds.includes(asset.id);
|
||||
});
|
||||
|
||||
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
|
||||
await this.albumRepository.save(album, {});
|
||||
|
||||
return res.affected || 0;
|
||||
return assetCount - album.assets.length;
|
||||
}
|
||||
|
||||
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> {
|
||||
const newRecords: AssetAlbumEntity[] = [];
|
||||
const alreadyExisting: string[] = [];
|
||||
|
||||
for (const assetId of addAssetsDto.assetIds) {
|
||||
// Album already contains that asset
|
||||
if (album.assets?.some((a) => a.assetId === assetId)) {
|
||||
if (album.assets?.some((a) => a.id === assetId)) {
|
||||
alreadyExisting.push(assetId);
|
||||
continue;
|
||||
}
|
||||
const newAssetAlbum = new AssetAlbumEntity();
|
||||
newAssetAlbum.assetId = assetId;
|
||||
newAssetAlbum.albumId = album.id;
|
||||
|
||||
newRecords.push(newAssetAlbum);
|
||||
album.assets.push({ id: assetId } as AssetEntity);
|
||||
}
|
||||
|
||||
// Add album thumbnail if not exist.
|
||||
if (!album.albumThumbnailAssetId && newRecords.length > 0) {
|
||||
album.albumThumbnailAssetId = newRecords[0].assetId;
|
||||
await this.albumRepository.save(album);
|
||||
if (!album.albumThumbnailAssetId && album.assets.length > 0) {
|
||||
album.albumThumbnailAssetId = album.assets[0].id;
|
||||
}
|
||||
|
||||
await this.assetAlbumRepository.save([...newRecords]);
|
||||
|
||||
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
|
||||
await this.albumRepository.save(album);
|
||||
|
||||
return {
|
||||
successfullyAdded: newRecords.length,
|
||||
successfullyAdded: addAssetsDto.assetIds.length - alreadyExisting.length,
|
||||
alreadyInAlbum: alreadyExisting,
|
||||
};
|
||||
}
|
||||
|
|
@ -330,19 +212,23 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
}
|
||||
|
||||
async getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number> {
|
||||
const result = await this.userAlbumRepository
|
||||
.createQueryBuilder('usa')
|
||||
.select('count(aa)', 'count')
|
||||
.innerJoin('asset_album', 'aa', 'aa.albumId = usa.albumId')
|
||||
.innerJoin('albums', 'a', 'a.id = usa.albumId')
|
||||
.where('aa.assetId = :assetId', { assetId })
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('a.ownerId = :userId', { userId }).orWhere('usa.sharedUserId = :userId', { userId });
|
||||
}),
|
||||
)
|
||||
.getRawOne();
|
||||
|
||||
return result.count;
|
||||
return this.albumRepository.count({
|
||||
where: [
|
||||
{
|
||||
ownerId: userId,
|
||||
assets: {
|
||||
id: assetId,
|
||||
},
|
||||
},
|
||||
{
|
||||
sharedUsers: {
|
||||
id: userId,
|
||||
},
|
||||
assets: {
|
||||
id: assetId,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AlbumService } from './album.service';
|
||||
import { AlbumController } from './album.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AlbumEntity, AssetAlbumEntity, UserAlbumEntity } from '@app/infra';
|
||||
import { AlbumEntity } from '@app/infra';
|
||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
|
||||
import { DownloadModule } from '../../modules/download/download.module';
|
||||
import { AssetModule } from '../asset/asset.module';
|
||||
|
||||
const ALBUM_REPOSITORY_PROVIDER = {
|
||||
provide: IAlbumRepository,
|
||||
|
|
@ -13,11 +12,7 @@ const ALBUM_REPOSITORY_PROVIDER = {
|
|||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AlbumEntity, AssetAlbumEntity, UserAlbumEntity]),
|
||||
DownloadModule,
|
||||
forwardRef(() => AssetModule),
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([AlbumEntity]), DownloadModule],
|
||||
controllers: [AlbumController],
|
||||
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
||||
exports: [ALBUM_REPOSITORY_PROVIDER],
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
|||
import { IAlbumRepository } from './album-repository';
|
||||
import { DownloadService } from '../../modules/download/download.service';
|
||||
import { ISharedLinkRepository } from '@app/domain';
|
||||
import { newCryptoRepositoryMock, newSharedLinkRepositoryMock } from '@app/domain/../test';
|
||||
import {
|
||||
assetEntityStub,
|
||||
newCryptoRepositoryMock,
|
||||
newSharedLinkRepositoryMock,
|
||||
userEntityStub,
|
||||
} from '@app/domain/../test';
|
||||
|
||||
describe('Album service', () => {
|
||||
let sut: AlbumService;
|
||||
|
|
@ -64,15 +69,8 @@ describe('Album service', () => {
|
|||
albumEntity.albumThumbnailAssetId = null;
|
||||
albumEntity.sharedUsers = [
|
||||
{
|
||||
id: '99',
|
||||
albumId,
|
||||
sharedUserId: ownedAlbumSharedWithId,
|
||||
//@ts-expect-error Partial stub
|
||||
albumInfo: {},
|
||||
//@ts-expect-error Partial stub
|
||||
userInfo: {
|
||||
id: ownedAlbumSharedWithId,
|
||||
},
|
||||
...userEntityStub.user1,
|
||||
id: ownedAlbumSharedWithId,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -90,26 +88,12 @@ describe('Album service', () => {
|
|||
albumEntity.albumThumbnailAssetId = null;
|
||||
albumEntity.sharedUsers = [
|
||||
{
|
||||
id: '99',
|
||||
albumId,
|
||||
sharedUserId: authUser.id,
|
||||
//@ts-expect-error Partial stub
|
||||
albumInfo: {},
|
||||
//@ts-expect-error Partial stub
|
||||
userInfo: {
|
||||
id: authUser.id,
|
||||
},
|
||||
...userEntityStub.user1,
|
||||
id: authUser.id,
|
||||
},
|
||||
{
|
||||
id: '98',
|
||||
albumId,
|
||||
sharedUserId: sharedAlbumSharedAlsoWithId,
|
||||
//@ts-expect-error Partial stub
|
||||
albumInfo: {},
|
||||
//@ts-expect-error Partial stub
|
||||
userInfo: {
|
||||
id: sharedAlbumSharedAlsoWithId,
|
||||
},
|
||||
...userEntityStub.user1,
|
||||
id: sharedAlbumSharedAlsoWithId,
|
||||
},
|
||||
];
|
||||
albumEntity.sharedLinks = [];
|
||||
|
|
@ -232,7 +216,7 @@ describe('Album service', () => {
|
|||
});
|
||||
|
||||
it('throws a not found exception if the album is not found', async () => {
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve(undefined));
|
||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve(null));
|
||||
await expect(sut.getAlbumInfo(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
|
|
@ -495,13 +479,8 @@ describe('Album service', () => {
|
|||
albumEntity.sharedUsers = [];
|
||||
albumEntity.assets = [
|
||||
{
|
||||
id: '1',
|
||||
albumId: '2',
|
||||
assetId: '3',
|
||||
//@ts-expect-error Partial stub
|
||||
albumInfo: {},
|
||||
//@ts-expect-error Partial stub
|
||||
assetInfo: {},
|
||||
...assetEntityStub.image,
|
||||
id: '3',
|
||||
},
|
||||
];
|
||||
albumEntity.albumThumbnailAssetId = null;
|
||||
|
|
@ -521,15 +500,7 @@ describe('Album service', () => {
|
|||
|
||||
albumEntity.albumThumbnailAssetId = 'nonexistent';
|
||||
assetEntity.id = newThumbnailAssetId;
|
||||
albumEntity.assets = [
|
||||
{
|
||||
id: '760841c1-f7c4-42b1-96af-c7d007a26126',
|
||||
assetId: assetEntity.id,
|
||||
albumId: albumEntity.id,
|
||||
albumInfo: albumEntity,
|
||||
assetInfo: assetEntity,
|
||||
},
|
||||
];
|
||||
albumEntity.assets = [assetEntity];
|
||||
albumRepositoryMock.getList.mockImplementation(async () => [albumEntity]);
|
||||
albumRepositoryMock.updateAlbum.mockImplementation(async () => ({
|
||||
...albumEntity,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class AlbumService {
|
|||
private shareCore: ShareCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||
private downloadService: DownloadService,
|
||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||
|
|
@ -40,7 +40,7 @@ export class AlbumService {
|
|||
albumId: string;
|
||||
validateIsOwner?: boolean;
|
||||
}): Promise<AlbumEntity> {
|
||||
const album = await this._albumRepository.get(albumId);
|
||||
const album = await this.albumRepository.get(albumId);
|
||||
if (!album) {
|
||||
throw new NotFoundException('Album Not Found');
|
||||
}
|
||||
|
|
@ -48,14 +48,14 @@ export class AlbumService {
|
|||
|
||||
if (validateIsOwner && !isOwner) {
|
||||
throw new ForbiddenException('Unauthorized Album Access');
|
||||
} else if (!isOwner && !album.sharedUsers?.some((user) => user.sharedUserId == authUser.id)) {
|
||||
} else if (!isOwner && !album.sharedUsers?.some((user) => user.id == authUser.id)) {
|
||||
throw new ForbiddenException('Unauthorized Album Access');
|
||||
}
|
||||
return album;
|
||||
}
|
||||
|
||||
async create(authUser: AuthUserDto, createAlbumDto: CreateAlbumDto): Promise<AlbumResponseDto> {
|
||||
const albumEntity = await this._albumRepository.create(authUser.id, createAlbumDto);
|
||||
const albumEntity = await this.albumRepository.create(authUser.id, createAlbumDto);
|
||||
return mapAlbum(albumEntity);
|
||||
}
|
||||
|
||||
|
|
@ -68,11 +68,11 @@ export class AlbumService {
|
|||
let albums: AlbumEntity[];
|
||||
|
||||
if (typeof getAlbumsDto.assetId === 'string') {
|
||||
albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
|
||||
albums = await this.albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
|
||||
} else {
|
||||
albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
||||
albums = await this.albumRepository.getList(authUser.id, getAlbumsDto);
|
||||
if (getAlbumsDto.shared) {
|
||||
const publicSharingAlbums = await this._albumRepository.getPublicSharingList(authUser.id);
|
||||
const publicSharingAlbums = await this.albumRepository.getPublicSharingList(authUser.id);
|
||||
albums = [...albums, ...publicSharingAlbums];
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ export class AlbumService {
|
|||
|
||||
async addUsersToAlbum(authUser: AuthUserDto, addUsersDto: AddUsersDto, albumId: string): Promise<AlbumResponseDto> {
|
||||
const album = await this._getAlbum({ authUser, albumId });
|
||||
const updatedAlbum = await this._albumRepository.addSharedUsers(album, addUsersDto);
|
||||
const updatedAlbum = await this.albumRepository.addSharedUsers(album, addUsersDto);
|
||||
return mapAlbum(updatedAlbum);
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ export class AlbumService {
|
|||
await this.shareCore.remove(authUser.id, sharedLink.id);
|
||||
}
|
||||
|
||||
await this._albumRepository.delete(album);
|
||||
await this.albumRepository.delete(album);
|
||||
}
|
||||
|
||||
async removeUserFromAlbum(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
|
||||
|
|
@ -116,7 +116,7 @@ export class AlbumService {
|
|||
if (album.ownerId == sharedUserId) {
|
||||
throw new BadRequestException('The owner of the album cannot be removed');
|
||||
}
|
||||
await this._albumRepository.removeUser(album, sharedUserId);
|
||||
await this.albumRepository.removeUser(album, sharedUserId);
|
||||
}
|
||||
|
||||
async removeAssetsFromAlbum(
|
||||
|
|
@ -125,7 +125,7 @@ export class AlbumService {
|
|||
albumId: string,
|
||||
): Promise<AlbumResponseDto> {
|
||||
const album = await this._getAlbum({ authUser, albumId });
|
||||
const deletedCount = await this._albumRepository.removeAssets(album, removeAssetsDto);
|
||||
const deletedCount = await this.albumRepository.removeAssets(album, removeAssetsDto);
|
||||
const newAlbum = await this._getAlbum({ authUser, albumId });
|
||||
|
||||
if (newAlbum) {
|
||||
|
|
@ -150,7 +150,7 @@ export class AlbumService {
|
|||
}
|
||||
|
||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||
const result = await this._albumRepository.addAssets(album, addAssetsDto);
|
||||
const result = await this.albumRepository.addAssets(album, addAssetsDto);
|
||||
const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||
|
||||
return {
|
||||
|
|
@ -170,17 +170,17 @@ export class AlbumService {
|
|||
throw new BadRequestException('Unauthorized to change album info');
|
||||
}
|
||||
|
||||
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
|
||||
const updatedAlbum = await this.albumRepository.updateAlbum(album, updateAlbumDto);
|
||||
return mapAlbum(updatedAlbum);
|
||||
}
|
||||
|
||||
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
||||
return this._albumRepository.getCountByUserId(authUser.id);
|
||||
return this.albumRepository.getCountByUserId(authUser.id);
|
||||
}
|
||||
|
||||
async downloadArchive(authUser: AuthUserDto, albumId: string, dto: DownloadDto) {
|
||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||
const assets = (album.assets || []).map((asset) => asset.assetInfo).slice(dto.skip || 0);
|
||||
const assets = (album.assets || []).map((asset) => asset).slice(dto.skip || 0);
|
||||
|
||||
return this.downloadService.downloadArchive(album.albumName, assets);
|
||||
}
|
||||
|
|
@ -190,16 +190,16 @@ export class AlbumService {
|
|||
|
||||
// Check if the album's thumbnail is invalid by referencing
|
||||
// an asset outside the album.
|
||||
const invalid = assets.length > 0 && !assets.some((asset) => asset.assetId === album.albumThumbnailAssetId);
|
||||
const invalid = assets.length > 0 && !assets.some((asset) => asset.id === album.albumThumbnailAssetId);
|
||||
|
||||
// Check if an empty album still has a thumbnail.
|
||||
const isEmptyWithThumbnail = assets.length === 0 && album.albumThumbnailAssetId !== null;
|
||||
|
||||
if (invalid || isEmptyWithThumbnail) {
|
||||
const albumThumbnailAssetId = assets[0]?.assetId;
|
||||
const albumThumbnailAssetId = assets[0]?.id;
|
||||
|
||||
album.albumThumbnailAssetId = albumThumbnailAssetId || null;
|
||||
await this._albumRepository.updateAlbum(album, { albumThumbnailAssetId });
|
||||
await this.albumRepository.updateAlbum(album, { albumThumbnailAssetId });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AssetService } from './asset.service';
|
||||
import { AssetController } from './asset.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
|
@ -22,7 +22,7 @@ const ASSET_REPOSITORY_PROVIDER = {
|
|||
DownloadModule,
|
||||
TagModule,
|
||||
StorageModule,
|
||||
forwardRef(() => AlbumModule),
|
||||
AlbumModule,
|
||||
],
|
||||
controllers: [AssetController],
|
||||
providers: [AssetService, ASSET_REPOSITORY_PROVIDER],
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
APIKeyUpdateDto,
|
||||
AuthUserDto,
|
||||
} from '@app/domain';
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, ValidationPipe } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
|
|
@ -31,21 +31,21 @@ export class APIKeyController {
|
|||
}
|
||||
|
||||
@Get(':id')
|
||||
getKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise<APIKeyResponseDto> {
|
||||
getKey(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<APIKeyResponseDto> {
|
||||
return this.service.getById(authUser, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
updateKey(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('id') id: string,
|
||||
@Body(ValidationPipe) dto: APIKeyUpdateDto,
|
||||
): Promise<APIKeyResponseDto> {
|
||||
return this.service.update(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise<void> {
|
||||
deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string): Promise<void> {
|
||||
return this.service.delete(authUser, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue