mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +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:
		| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -66,7 +66,7 @@ | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "type": "number" | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
| @@ -95,7 +95,7 @@ | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "type": "number" | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
| @@ -134,7 +134,7 @@ | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "type": "number" | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
| @@ -2759,7 +2759,7 @@ | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "id": { | ||||
|             "type": "integer" | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "name": { | ||||
|             "type": "string" | ||||
|   | ||||
| @@ -21,11 +21,9 @@ export class AlbumResponseDto { | ||||
| export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { | ||||
|   const sharedUsers: UserResponseDto[] = []; | ||||
|  | ||||
|   entity.sharedUsers?.forEach((userAlbum) => { | ||||
|     if (userAlbum.userInfo) { | ||||
|       const user = mapUser(userAlbum.userInfo); | ||||
|       sharedUsers.push(user); | ||||
|     } | ||||
|   entity.sharedUsers?.forEach((user) => { | ||||
|     const userDto = mapUser(user); | ||||
|     sharedUsers.push(userDto); | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
| @@ -38,7 +36,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { | ||||
|     owner: mapUser(entity.owner), | ||||
|     sharedUsers, | ||||
|     shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0, | ||||
|     assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [], | ||||
|     assets: entity.assets?.map((asset) => mapAsset(asset)) || [], | ||||
|     assetCount: entity.assets?.length || 0, | ||||
|   }; | ||||
| } | ||||
| @@ -46,11 +44,9 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { | ||||
| export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto { | ||||
|   const sharedUsers: UserResponseDto[] = []; | ||||
|  | ||||
|   entity.sharedUsers?.forEach((userAlbum) => { | ||||
|     if (userAlbum.userInfo) { | ||||
|       const user = mapUser(userAlbum.userInfo); | ||||
|       sharedUsers.push(user); | ||||
|     } | ||||
|   entity.sharedUsers?.forEach((user) => { | ||||
|     const userDto = mapUser(user); | ||||
|     sharedUsers.push(userDto); | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -4,13 +4,13 @@ export const IKeyRepository = 'IKeyRepository'; | ||||
|  | ||||
| export interface IKeyRepository { | ||||
|   create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>; | ||||
|   update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>; | ||||
|   delete(userId: string, id: number): Promise<void>; | ||||
|   update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>; | ||||
|   delete(userId: string, id: string): Promise<void>; | ||||
|   /** | ||||
|    * Includes the hashed `key` for verification | ||||
|    * @param id | ||||
|    */ | ||||
|   getKey(hashedToken: string): Promise<APIKeyEntity | null>; | ||||
|   getById(userId: string, id: number): Promise<APIKeyEntity | null>; | ||||
|   getById(userId: string, id: string): Promise<APIKeyEntity | null>; | ||||
|   getByUserId(userId: string): Promise<APIKeyEntity[]>; | ||||
| } | ||||
|   | ||||
| @@ -47,17 +47,19 @@ describe(APIKeyService.name, () => { | ||||
|     it('should throw an error if the key is not found', async () => { | ||||
|       keyMock.getById.mockResolvedValue(null); | ||||
|  | ||||
|       await expect(sut.update(authStub.admin, 1, { name: 'New Name' })).rejects.toBeInstanceOf(BadRequestException); | ||||
|       await expect(sut.update(authStub.admin, 'random-guid', { name: 'New Name' })).rejects.toBeInstanceOf( | ||||
|         BadRequestException, | ||||
|       ); | ||||
|  | ||||
|       expect(keyMock.update).not.toHaveBeenCalledWith(1); | ||||
|       expect(keyMock.update).not.toHaveBeenCalledWith('random-guid'); | ||||
|     }); | ||||
|  | ||||
|     it('should update a key', async () => { | ||||
|       keyMock.getById.mockResolvedValue(keyStub.admin); | ||||
|  | ||||
|       await sut.update(authStub.admin, 1, { name: 'New Name' }); | ||||
|       await sut.update(authStub.admin, 'random-guid', { name: 'New Name' }); | ||||
|  | ||||
|       expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.id, 1, { name: 'New Name' }); | ||||
|       expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.id, 'random-guid', { name: 'New Name' }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @@ -65,17 +67,17 @@ describe(APIKeyService.name, () => { | ||||
|     it('should throw an error if the key is not found', async () => { | ||||
|       keyMock.getById.mockResolvedValue(null); | ||||
|  | ||||
|       await expect(sut.delete(authStub.admin, 1)).rejects.toBeInstanceOf(BadRequestException); | ||||
|       await expect(sut.delete(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(keyMock.delete).not.toHaveBeenCalledWith(1); | ||||
|       expect(keyMock.delete).not.toHaveBeenCalledWith('random-guid'); | ||||
|     }); | ||||
|  | ||||
|     it('should delete a key', async () => { | ||||
|       keyMock.getById.mockResolvedValue(keyStub.admin); | ||||
|  | ||||
|       await sut.delete(authStub.admin, 1); | ||||
|       await sut.delete(authStub.admin, 'random-guid'); | ||||
|  | ||||
|       expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.id, 1); | ||||
|       expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.id, 'random-guid'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @@ -83,17 +85,17 @@ describe(APIKeyService.name, () => { | ||||
|     it('should throw an error if the key is not found', async () => { | ||||
|       keyMock.getById.mockResolvedValue(null); | ||||
|  | ||||
|       await expect(sut.getById(authStub.admin, 1)).rejects.toBeInstanceOf(BadRequestException); | ||||
|       await expect(sut.getById(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 1); | ||||
|       expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'random-guid'); | ||||
|     }); | ||||
|  | ||||
|     it('should get a key by id', async () => { | ||||
|       keyMock.getById.mockResolvedValue(keyStub.admin); | ||||
|  | ||||
|       await sut.getById(authStub.admin, 1); | ||||
|       await sut.getById(authStub.admin, 'random-guid'); | ||||
|  | ||||
|       expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 1); | ||||
|       expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'random-guid'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ export class APIKeyService { | ||||
|     return { secret, apiKey: mapKey(entity) }; | ||||
|   } | ||||
|  | ||||
|   async update(authUser: AuthUserDto, id: number, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> { | ||||
|   async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> { | ||||
|     const exists = await this.repository.getById(authUser.id, id); | ||||
|     if (!exists) { | ||||
|       throw new BadRequestException('API Key not found'); | ||||
| @@ -35,7 +35,7 @@ export class APIKeyService { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async delete(authUser: AuthUserDto, id: number): Promise<void> { | ||||
|   async delete(authUser: AuthUserDto, id: string): Promise<void> { | ||||
|     const exists = await this.repository.getById(authUser.id, id); | ||||
|     if (!exists) { | ||||
|       throw new BadRequestException('API Key not found'); | ||||
| @@ -44,7 +44,7 @@ export class APIKeyService { | ||||
|     await this.repository.delete(authUser.id, id); | ||||
|   } | ||||
|  | ||||
|   async getById(authUser: AuthUserDto, id: number): Promise<APIKeyResponseDto> { | ||||
|   async getById(authUser: AuthUserDto, id: string): Promise<APIKeyResponseDto> { | ||||
|     const key = await this.repository.getById(authUser.id, id); | ||||
|     if (!key) { | ||||
|       throw new BadRequestException('API Key not found'); | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| import { APIKeyEntity } from '@app/infra/db/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
|  | ||||
| export class APIKeyResponseDto { | ||||
|   @ApiProperty({ type: 'integer' }) | ||||
|   id!: number; | ||||
|   id!: string; | ||||
|   name!: string; | ||||
|   createdAt!: string; | ||||
|   updatedAt!: string; | ||||
|   | ||||
| @@ -23,7 +23,7 @@ export class SharedLinkResponseDto { | ||||
|  | ||||
| export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||
|   const linkAssets = sharedLink.assets || []; | ||||
|   const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); | ||||
|   const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); | ||||
|  | ||||
|   const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id); | ||||
|  | ||||
| @@ -45,7 +45,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD | ||||
|  | ||||
| export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto { | ||||
|   const linkAssets = sharedLink.assets || []; | ||||
|   const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); | ||||
|   const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); | ||||
|  | ||||
|   const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { | ||||
|   APIKeyEntity, | ||||
|   AssetEntity, | ||||
|   AssetType, | ||||
|   SharedLinkEntity, | ||||
|   SharedLinkType, | ||||
| @@ -90,6 +91,30 @@ export const userEntityStub = { | ||||
|   }), | ||||
| }; | ||||
|  | ||||
| export const assetEntityStub = { | ||||
|   image: Object.freeze<AssetEntity>({ | ||||
|     id: 'asset-id', | ||||
|     deviceAssetId: 'device-asset-id', | ||||
|     modifiedAt: today.toISOString(), | ||||
|     createdAt: today.toISOString(), | ||||
|     userId: 'user-id', | ||||
|     deviceId: 'device-id', | ||||
|     originalPath: '/original/path', | ||||
|     resizePath: null, | ||||
|     type: AssetType.IMAGE, | ||||
|     webpPath: null, | ||||
|     encodedVideoPath: null, | ||||
|     updatedAt: today.toISOString(), | ||||
|     mimeType: null, | ||||
|     isFavorite: true, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideoId: null, | ||||
|     tags: [], | ||||
|     sharedLinks: [], | ||||
|   }), | ||||
| }; | ||||
|  | ||||
| const assetInfo: ExifResponseDto = { | ||||
|   id: 1, | ||||
|   make: 'camera-make', | ||||
| @@ -165,7 +190,7 @@ export const userTokenEntityStub = { | ||||
|  | ||||
| export const keyStub = { | ||||
|   admin: Object.freeze({ | ||||
|     id: 1, | ||||
|     id: 'my-random-guid', | ||||
|     name: 'My Key', | ||||
|     key: 'my-api-key (hashed)', | ||||
|     userId: authStub.admin.id, | ||||
| @@ -348,66 +373,60 @@ export const sharedLinkStub = { | ||||
|       sharedLinks: [], | ||||
|       assets: [ | ||||
|         { | ||||
|           id: 'album-asset-123', | ||||
|           albumId: 'album-123', | ||||
|           assetId: 'asset-123', | ||||
|           albumInfo: {} as any, | ||||
|           assetInfo: { | ||||
|             id: 'id_1', | ||||
|             userId: 'user_id_1', | ||||
|             deviceAssetId: 'device_asset_id_1', | ||||
|             deviceId: 'device_id_1', | ||||
|             type: AssetType.VIDEO, | ||||
|             originalPath: 'fake_path/jpeg', | ||||
|             resizePath: '', | ||||
|             createdAt: today.toISOString(), | ||||
|             modifiedAt: today.toISOString(), | ||||
|             updatedAt: today.toISOString(), | ||||
|             isFavorite: false, | ||||
|             mimeType: 'image/jpeg', | ||||
|             smartInfo: { | ||||
|               id: 'should-be-a-number', | ||||
|               assetId: 'id_1', | ||||
|               tags: [], | ||||
|               objects: ['a', 'b', 'c'], | ||||
|               asset: null as any, | ||||
|             }, | ||||
|             webpPath: '', | ||||
|             encodedVideoPath: '', | ||||
|             duration: null, | ||||
|             isVisible: true, | ||||
|             livePhotoVideoId: null, | ||||
|             exifInfo: { | ||||
|               livePhotoCID: null, | ||||
|               id: 1, | ||||
|               assetId: 'id_1', | ||||
|               description: 'description', | ||||
|               exifImageWidth: 500, | ||||
|               exifImageHeight: 500, | ||||
|               fileSizeInByte: 100, | ||||
|               orientation: 'orientation', | ||||
|               dateTimeOriginal: today, | ||||
|               modifyDate: today, | ||||
|               latitude: 100, | ||||
|               longitude: 100, | ||||
|               city: 'city', | ||||
|               state: 'state', | ||||
|               country: 'country', | ||||
|               make: 'camera-make', | ||||
|               model: 'camera-model', | ||||
|               imageName: 'fancy-image', | ||||
|               lensModel: 'fancy', | ||||
|               fNumber: 100, | ||||
|               focalLength: 100, | ||||
|               iso: 100, | ||||
|               exposureTime: '1/16', | ||||
|               fps: 100, | ||||
|               asset: null as any, | ||||
|               exifTextSearchableColumn: '', | ||||
|             }, | ||||
|           id: 'id_1', | ||||
|           userId: 'user_id_1', | ||||
|           deviceAssetId: 'device_asset_id_1', | ||||
|           deviceId: 'device_id_1', | ||||
|           type: AssetType.VIDEO, | ||||
|           originalPath: 'fake_path/jpeg', | ||||
|           resizePath: '', | ||||
|           createdAt: today.toISOString(), | ||||
|           modifiedAt: today.toISOString(), | ||||
|           updatedAt: today.toISOString(), | ||||
|           isFavorite: false, | ||||
|           mimeType: 'image/jpeg', | ||||
|           smartInfo: { | ||||
|             id: 'should-be-a-number', | ||||
|             assetId: 'id_1', | ||||
|             tags: [], | ||||
|             sharedLinks: [], | ||||
|             objects: ['a', 'b', 'c'], | ||||
|             asset: null as any, | ||||
|           }, | ||||
|           webpPath: '', | ||||
|           encodedVideoPath: '', | ||||
|           duration: null, | ||||
|           isVisible: true, | ||||
|           livePhotoVideoId: null, | ||||
|           exifInfo: { | ||||
|             livePhotoCID: null, | ||||
|             id: 1, | ||||
|             assetId: 'id_1', | ||||
|             description: 'description', | ||||
|             exifImageWidth: 500, | ||||
|             exifImageHeight: 500, | ||||
|             fileSizeInByte: 100, | ||||
|             orientation: 'orientation', | ||||
|             dateTimeOriginal: today, | ||||
|             modifyDate: today, | ||||
|             latitude: 100, | ||||
|             longitude: 100, | ||||
|             city: 'city', | ||||
|             state: 'state', | ||||
|             country: 'country', | ||||
|             make: 'camera-make', | ||||
|             model: 'camera-model', | ||||
|             imageName: 'fancy-image', | ||||
|             lensModel: 'fancy', | ||||
|             fNumber: 100, | ||||
|             focalLength: 100, | ||||
|             iso: 100, | ||||
|             exposureTime: '1/16', | ||||
|             fps: 100, | ||||
|             asset: null as any, | ||||
|             exifTextSearchableColumn: '', | ||||
|           }, | ||||
|           tags: [], | ||||
|           sharedLinks: [], | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   | ||||
| @@ -2,14 +2,15 @@ import { | ||||
|   Column, | ||||
|   CreateDateColumn, | ||||
|   Entity, | ||||
|   JoinTable, | ||||
|   ManyToMany, | ||||
|   ManyToOne, | ||||
|   OneToMany, | ||||
|   PrimaryGeneratedColumn, | ||||
|   UpdateDateColumn, | ||||
| } from 'typeorm'; | ||||
| import { AssetAlbumEntity } from './asset-album.entity'; | ||||
| import { SharedLinkEntity } from './shared-link.entity'; | ||||
| import { UserAlbumEntity } from './user-album.entity'; | ||||
| import { AssetEntity } from './asset.entity'; | ||||
| import { UserEntity } from './user.entity'; | ||||
|  | ||||
| @Entity('albums') | ||||
| @@ -17,12 +18,12 @@ export class AlbumEntity { | ||||
|   @PrimaryGeneratedColumn('uuid') | ||||
|   id!: string; | ||||
|  | ||||
|   @ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) | ||||
|   owner!: UserEntity; | ||||
|  | ||||
|   @Column() | ||||
|   ownerId!: string; | ||||
|  | ||||
|   @ManyToOne(() => UserEntity, { eager: true }) | ||||
|   owner!: UserEntity; | ||||
|  | ||||
|   @Column({ default: 'Untitled Album' }) | ||||
|   albumName!: string; | ||||
|  | ||||
| @@ -35,11 +36,13 @@ export class AlbumEntity { | ||||
|   @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true }) | ||||
|   albumThumbnailAssetId!: string | null; | ||||
|  | ||||
|   @OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo) | ||||
|   sharedUsers?: UserAlbumEntity[]; | ||||
|   @ManyToMany(() => UserEntity, { eager: true }) | ||||
|   @JoinTable() | ||||
|   sharedUsers!: UserEntity[]; | ||||
|  | ||||
|   @OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo) | ||||
|   assets?: AssetAlbumEntity[]; | ||||
|   @ManyToMany(() => AssetEntity, { eager: true }) | ||||
|   @JoinTable() | ||||
|   assets!: AssetEntity[]; | ||||
|  | ||||
|   @OneToMany(() => SharedLinkEntity, (link) => link.album) | ||||
|   sharedLinks!: SharedLinkEntity[]; | ||||
|   | ||||
| @@ -3,8 +3,8 @@ import { UserEntity } from './user.entity'; | ||||
|  | ||||
| @Entity('api_keys') | ||||
| export class APIKeyEntity { | ||||
|   @PrimaryGeneratedColumn() | ||||
|   id!: number; | ||||
|   @PrimaryGeneratedColumn('uuid') | ||||
|   id!: string; | ||||
|  | ||||
|   @Column() | ||||
|   name!: string; | ||||
| @@ -12,12 +12,12 @@ export class APIKeyEntity { | ||||
|   @Column({ select: false }) | ||||
|   key?: string; | ||||
|  | ||||
|   @Column() | ||||
|   userId!: string; | ||||
|  | ||||
|   @ManyToOne(() => UserEntity) | ||||
|   user?: UserEntity; | ||||
|  | ||||
|   @Column() | ||||
|   userId!: string; | ||||
|  | ||||
|   @CreateDateColumn({ type: 'timestamptz' }) | ||||
|   createdAt!: string; | ||||
|  | ||||
|   | ||||
| @@ -1,31 +0,0 @@ | ||||
| import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; | ||||
| import { AlbumEntity } from './album.entity'; | ||||
| import { AssetEntity } from './asset.entity'; | ||||
|  | ||||
| @Entity('asset_album') | ||||
| @Unique('UQ_unique_asset_in_album', ['albumId', 'assetId']) | ||||
| export class AssetAlbumEntity { | ||||
|   @PrimaryGeneratedColumn() | ||||
|   id!: string; | ||||
|  | ||||
|   @Column() | ||||
|   albumId!: string; | ||||
|  | ||||
|   @Column() | ||||
|   @OneToOne(() => AssetEntity, (entity) => entity.id) | ||||
|   assetId!: string; | ||||
|  | ||||
|   @ManyToOne(() => AlbumEntity, (album) => album.assets, { | ||||
|     onDelete: 'CASCADE', | ||||
|     nullable: true, | ||||
|   }) | ||||
|   @JoinColumn({ name: 'albumId' }) | ||||
|   albumInfo!: AlbumEntity; | ||||
|  | ||||
|   @ManyToOne(() => AssetEntity, { | ||||
|     onDelete: 'CASCADE', | ||||
|     nullable: true, | ||||
|   }) | ||||
|   @JoinColumn({ name: 'assetId' }) | ||||
|   assetInfo!: AssetEntity; | ||||
| } | ||||
| @@ -1,13 +1,11 @@ | ||||
| export * from './album.entity'; | ||||
| export * from './api-key.entity'; | ||||
| export * from './asset-album.entity'; | ||||
| export * from './asset.entity'; | ||||
| export * from './device-info.entity'; | ||||
| export * from './exif.entity'; | ||||
| export * from './smart-info.entity'; | ||||
| export * from './system-config.entity'; | ||||
| export * from './tag.entity'; | ||||
| export * from './user-album.entity'; | ||||
| export * from './user.entity'; | ||||
| export * from './user-token.entity'; | ||||
| export * from './shared-link.entity'; | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; | ||||
| import { UserEntity } from './user.entity'; | ||||
| import { AlbumEntity } from './album.entity'; | ||||
|  | ||||
| @Entity('user_shared_album') | ||||
| @Unique('PK_unique_user_in_album', ['albumId', 'sharedUserId']) | ||||
| export class UserAlbumEntity { | ||||
|   @PrimaryGeneratedColumn() | ||||
|   id!: string; | ||||
|  | ||||
|   @Column() | ||||
|   albumId!: string; | ||||
|  | ||||
|   @Column() | ||||
|   sharedUserId!: string; | ||||
|  | ||||
|   @ManyToOne(() => AlbumEntity, (album) => album.sharedUsers, { | ||||
|     onDelete: 'CASCADE', | ||||
|     nullable: true, | ||||
|   }) | ||||
|   @JoinColumn({ name: 'albumId' }) | ||||
|   albumInfo!: AlbumEntity; | ||||
|  | ||||
|   @ManyToOne(() => UserEntity) | ||||
|   @JoinColumn({ name: 'sharedUserId' }) | ||||
|   userInfo!: UserEntity; | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||||
|  | ||||
| export class APIKeyUUIDPrimaryKey1675808874445 implements MigrationInterface { | ||||
|     name = 'APIKeyUUIDPrimaryKey1675808874445' | ||||
|  | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad"`); | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" DROP COLUMN "id"`); | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" ADD "id" uuid NOT NULL DEFAULT uuid_generate_v4()`); | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id")`); | ||||
|     } | ||||
|  | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad"`); | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" DROP COLUMN "id"`); | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" ADD "id" SERIAL NOT NULL`); | ||||
|         await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id")`); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,82 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||||
|  | ||||
| export class FixAlbumEntityTypeORM1675812532822 implements MigrationInterface { | ||||
|     name = 'FixAlbumEntityTypeORM1675812532822' | ||||
|  | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|  | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" RENAME TO "albums_assets_assets"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP CONSTRAINT "UQ_unique_asset_in_album"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP CONSTRAINT "PK_a34e076afbc601d81938e2c2277"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP COLUMN "id"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" RENAME COLUMN "albumId" TO "albumsId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" RENAME COLUMN "assetId" TO "assetsId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId")`); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_e590fa396c6898fcd4a50e4092" ON "albums_assets_assets" ("albumsId") `); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_4bd1303d199f4e72ccdf998c62" ON "albums_assets_assets" ("assetsId") `); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE CASCADE`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); | ||||
|  | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" RENAME TO "albums_shared_users_users"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP CONSTRAINT "FK_543c31211653e63e080ba882eb5"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP CONSTRAINT "FK_7b3bf0f5f8da59af30519c25f18"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP CONSTRAINT "PK_unique_user_in_album"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP CONSTRAINT "PK_b6562316a98845a7b3e9a25cdd0"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "id"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" RENAME COLUMN "albumId" TO "albumsId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" RENAME COLUMN "sharedUserId" TO "usersId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId")`); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_427c350ad49bd3935a50baab73" ON "albums_shared_users_users" ("albumsId") `); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_f48513bf9bccefd6ff3ad30bd0" ON "albums_shared_users_users" ("usersId") `); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE CASCADE`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); | ||||
|  | ||||
|         await queryRunner.query(`ALTER TABLE "albums" DROP CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4"`) | ||||
|         await queryRunner.query(`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); | ||||
|     } | ||||
|  | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "albums_assets_assets" RENAME TO "asset_album"`); | ||||
|         await queryRunner.query(`ALTER TABLE "albums_shared_users_users" RENAME TO "user_shared_album"`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_e590fa396c6898fcd4a50e40927"`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_427c350ad49bd3935a50baab737"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06"`); | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_427c350ad49bd3935a50baab73"`); | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_f48513bf9bccefd6ff3ad30bd0"`); | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_e590fa396c6898fcd4a50e4092"`); | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_4bd1303d199f4e72ccdf998c62"`); | ||||
|  | ||||
|         await queryRunner.query(`ALTER TABLE "albums" DROP CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4"`); | ||||
|         await queryRunner.query( | ||||
|           `ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, | ||||
|         ); | ||||
|  | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD CONSTRAINT "PK_323f8dcbe85373722886940f143" PRIMARY KEY ("albumsId")`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" DROP COLUMN "usersId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "PK_323f8dcbe85373722886940f143"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" DROP COLUMN "albumsId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD "sharedUserId" uuid NOT NULL`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD "albumId" uuid NOT NULL`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD "id" SERIAL NOT NULL`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD CONSTRAINT "PK_b6562316a98845a7b3e9a25cdd0" PRIMARY KEY ("id")`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD CONSTRAINT "PK_unique_user_in_album" UNIQUE ("albumId", "sharedUserId")`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD CONSTRAINT "FK_7b3bf0f5f8da59af30519c25f18" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||
|         await queryRunner.query(`ALTER TABLE "user_shared_album" ADD CONSTRAINT "FK_543c31211653e63e080ba882eb5" FOREIGN KEY ("sharedUserId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); | ||||
|  | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180"`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "PK_b4f2e5b96efc25cbccd80a04f7a" PRIMARY KEY ("albumsId")`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "PK_b4f2e5b96efc25cbccd80a04f7a"`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" RENAME COLUMN "albumsId" TO "albumId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" RENAME COLUMN "assetsId" TO "assetId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" ADD "id" SERIAL NOT NULL`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "PK_a34e076afbc601d81938e2c2277" PRIMARY KEY ("id")`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "UQ_unique_asset_in_album" UNIQUE ("albumId", "assetId")`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_256a30a03a4a0aff0394051397d" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||
|         await queryRunner.query(`ALTER TABLE "asset_album" ADD CONSTRAINT "FK_7ae4e03729895bf87e056d7b598" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -12,12 +12,12 @@ export class APIKeyRepository implements IKeyRepository { | ||||
|     return this.repository.save(dto); | ||||
|   } | ||||
|  | ||||
|   async update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> { | ||||
|   async update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> { | ||||
|     await this.repository.update({ userId, id }, dto); | ||||
|     return this.repository.findOneOrFail({ where: { id: dto.id } }); | ||||
|   } | ||||
|  | ||||
|   async delete(userId: string, id: number): Promise<void> { | ||||
|   async delete(userId: string, id: string): Promise<void> { | ||||
|     await this.repository.delete({ userId, id }); | ||||
|   } | ||||
|  | ||||
| @@ -35,7 +35,7 @@ export class APIKeyRepository implements IKeyRepository { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getById(userId: string, id: number): Promise<APIKeyEntity | null> { | ||||
|   getById(userId: string, id: string): Promise<APIKeyEntity | null> { | ||||
|     return this.repository.findOne({ where: { userId, id } }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -24,9 +24,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { | ||||
|         }, | ||||
|         album: { | ||||
|           assets: { | ||||
|             assetInfo: { | ||||
|               exifInfo: true, | ||||
|             }, | ||||
|             exifInfo: true, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
| @@ -37,9 +35,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { | ||||
|         }, | ||||
|         album: { | ||||
|           assets: { | ||||
|             assetInfo: { | ||||
|               createdAt: 'ASC', | ||||
|             }, | ||||
|             createdAt: 'ASC', | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
| @@ -69,9 +65,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { | ||||
|       relations: { | ||||
|         assets: true, | ||||
|         album: { | ||||
|           assets: { | ||||
|             assetInfo: true, | ||||
|           }, | ||||
|           assets: true, | ||||
|         }, | ||||
|         user: true, | ||||
|       }, | ||||
| @@ -109,7 +103,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { | ||||
|         id, | ||||
|         album: { | ||||
|           assets: { | ||||
|             assetId, | ||||
|             id: assetId, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|   | ||||
| @@ -33,6 +33,7 @@ | ||||
|     "typeorm:migrations:run": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:run -d ./libs/infra/src/db/config/database.config.ts", | ||||
|     "typeorm:migrations:revert": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:revert -d ./libs/infra/src/db/config/database.config.ts", | ||||
|     "typeorm:schema:drop": "node --require ts-node/register ./node_modules/typeorm/cli.js schema:drop -d ./libs/infra/src/db/config/database.config.ts", | ||||
|     "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run", | ||||
|     "api:typescript": "bash ./bin/generate-open-api.sh web", | ||||
|     "api:dart": "bash ./bin/generate-open-api.sh mobile", | ||||
|     "api:generate": "bash ./bin/generate-open-api.sh" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user