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