mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
infra(server): fix Album TypeORM relations and change ids to uuids (#1582)
* infra: make api-key primary key column a UUID * infra: move ManyToMany relations in album entity, make ownerId ManyToOne --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
917f1dea9f
commit
000d0a08f4
24 changed files with 368 additions and 461 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { AlbumEntity, AssetAlbumEntity, UserAlbumEntity } from '@app/infra';
|
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||||
|
@ -15,7 +15,7 @@ export interface IAlbumRepository {
|
||||||
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
||||||
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>;
|
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>;
|
||||||
getPublicSharingList(ownerId: string): Promise<AlbumEntity[]>;
|
getPublicSharingList(ownerId: string): Promise<AlbumEntity[]>;
|
||||||
get(albumId: string): Promise<AlbumEntity | undefined>;
|
get(albumId: string): Promise<AlbumEntity | null>;
|
||||||
delete(album: AlbumEntity): Promise<void>;
|
delete(album: AlbumEntity): Promise<void>;
|
||||||
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
||||||
removeUser(album: AlbumEntity, userId: string): Promise<void>;
|
removeUser(album: AlbumEntity, userId: string): Promise<void>;
|
||||||
|
@ -34,14 +34,6 @@ export class AlbumRepository implements IAlbumRepository {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AlbumEntity)
|
@InjectRepository(AlbumEntity)
|
||||||
private albumRepository: Repository<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[]> {
|
async getPublicSharingList(ownerId: string): Promise<AlbumEntity[]> {
|
||||||
|
@ -62,194 +54,98 @@ export class AlbumRepository implements IAlbumRepository {
|
||||||
|
|
||||||
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
|
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
|
||||||
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
|
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
|
||||||
|
const sharedAlbums = await this.albumRepository.count({ where: { sharedUsers: { id: userId } } });
|
||||||
const sharedAlbums = await this.userAlbumRepository.count({
|
const sharedAlbumCount = ownedAlbums.filter((album) => album.sharedUsers?.length > 0).length;
|
||||||
where: { sharedUserId: userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
let sharedAlbumCount = 0;
|
|
||||||
ownedAlbums.map((album) => {
|
|
||||||
if (album.sharedUsers?.length) {
|
|
||||||
sharedAlbumCount += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharedAlbumCount);
|
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharedAlbumCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
|
async create(ownerId: string, dto: CreateAlbumDto): Promise<AlbumEntity> {
|
||||||
return this.dataSource.transaction(async (transactionalEntityManager) => {
|
const album = await this.albumRepository.save({
|
||||||
// Create album entity
|
ownerId,
|
||||||
const newAlbum = new AlbumEntity();
|
albumName: dto.albumName,
|
||||||
newAlbum.ownerId = ownerId;
|
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
|
||||||
newAlbum.albumName = createAlbumDto.albumName;
|
assets: dto.assetIds?.map((value) => ({ id: value } as AssetEntity)) ?? [],
|
||||||
|
albumThumbnailAssetId: dto.assetIds?.[0] || null,
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// need to re-load the relations
|
||||||
|
return this.get(album.id) as Promise<AlbumEntity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
async getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
||||||
const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
|
const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
|
||||||
const userId = ownerId;
|
const userId = ownerId;
|
||||||
let query = this.albumRepository.createQueryBuilder('album');
|
|
||||||
|
|
||||||
const getSharedAlbumIdsSubQuery = (qb: SelectQueryBuilder<AlbumEntity>) => {
|
const queryProperties: FindManyOptions<AlbumEntity> = {
|
||||||
return qb
|
relations: { sharedUsers: true, assets: true, sharedLinks: true, owner: true },
|
||||||
.subQuery()
|
order: { assets: { createdAt: 'ASC' }, createdAt: 'ASC' },
|
||||||
.select('albumSub.id')
|
|
||||||
.from(AlbumEntity, 'albumSub')
|
|
||||||
.innerJoin('albumSub.sharedUsers', 'userAlbumSub')
|
|
||||||
.where('albumSub.ownerId = :ownerId', { ownerId: userId })
|
|
||||||
.getQuery();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (filteringByShared) {
|
||||||
if (getAlbumsDto.shared) {
|
if (getAlbumsDto.shared) {
|
||||||
// shared albums
|
// shared albums
|
||||||
query = query
|
albumsQuery = this.albumRepository.find({
|
||||||
.innerJoinAndSelect('album.sharedUsers', 'sharedUser')
|
where: [{ sharedUsers: { id: userId } }, { ownerId: userId, sharedUsers: { id: Not(IsNull()) } }],
|
||||||
.innerJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
...queryProperties,
|
||||||
.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}`;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// owned, not shared albums
|
// owned, not shared albums
|
||||||
query = query.where('album.ownerId = :ownerId', { ownerId: userId }).andWhere((qb) => {
|
albumsQuery = this.albumRepository.find({
|
||||||
const subQuery = getSharedAlbumIdsSubQuery(qb);
|
where: { ownerId: userId, sharedUsers: { id: IsNull() } },
|
||||||
return `album.id NOT IN ${subQuery}`;
|
...queryProperties,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// owned and shared with userId
|
// owned
|
||||||
query = query
|
albumsQuery = this.albumRepository.find({
|
||||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
where: { ownerId: userId },
|
||||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
...queryProperties,
|
||||||
.where('album.ownerId = :ownerId', { ownerId: userId });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get information of assets in albums
|
const albums = await albumsQuery;
|
||||||
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();
|
|
||||||
|
|
||||||
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
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[]> {
|
async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||||
const query = this.albumRepository.createQueryBuilder('album');
|
const albums = await this.albumRepository.find({
|
||||||
|
where: { ownerId: userId, assets: { id: assetId } },
|
||||||
const albums = await query
|
relations: { owner: true, assets: true, sharedUsers: true },
|
||||||
.where('album.ownerId = :ownerId', { ownerId: userId })
|
order: { assets: { createdAt: 'ASC' } },
|
||||||
.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();
|
|
||||||
|
|
||||||
return albums;
|
return albums;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
async get(albumId: string): Promise<AlbumEntity | null> {
|
||||||
const album = await this.albumRepository.findOne({
|
return this.albumRepository.findOne({
|
||||||
where: { id: albumId },
|
where: { id: albumId },
|
||||||
relations: {
|
relations: {
|
||||||
owner: true,
|
owner: true,
|
||||||
sharedUsers: {
|
sharedUsers: true,
|
||||||
userInfo: true,
|
|
||||||
},
|
|
||||||
assets: {
|
assets: {
|
||||||
assetInfo: {
|
exifInfo: true,
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
sharedLinks: true,
|
sharedLinks: true,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
assets: {
|
assets: {
|
||||||
assetInfo: {
|
createdAt: 'ASC',
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!album) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return album;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(album: AlbumEntity): Promise<void> {
|
async delete(album: AlbumEntity): Promise<void> {
|
||||||
|
@ -257,67 +153,53 @@ export class AlbumRepository implements IAlbumRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity> {
|
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) {
|
await this.albumRepository.save(album);
|
||||||
const newEntity = new UserAlbumEntity();
|
|
||||||
newEntity.albumId = album.id;
|
|
||||||
newEntity.sharedUserId = sharedUserId;
|
|
||||||
|
|
||||||
newRecords.push(newEntity);
|
// need to re-load the shared user relation
|
||||||
}
|
return this.get(album.id) as Promise<AlbumEntity>;
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeUser(album: AlbumEntity, userId: string): Promise<void> {
|
async removeUser(album: AlbumEntity, userId: string): Promise<void> {
|
||||||
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId });
|
album.sharedUsers = album.sharedUsers.filter((user) => user.id !== userId);
|
||||||
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
|
await this.albumRepository.save(album);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
|
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
|
||||||
const res = await this.assetAlbumRepository.delete({
|
const assetCount = album.assets.length;
|
||||||
albumId: album.id,
|
|
||||||
assetId: In(removeAssetsDto.assetIds),
|
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> {
|
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> {
|
||||||
const newRecords: AssetAlbumEntity[] = [];
|
|
||||||
const alreadyExisting: string[] = [];
|
const alreadyExisting: string[] = [];
|
||||||
|
|
||||||
for (const assetId of addAssetsDto.assetIds) {
|
for (const assetId of addAssetsDto.assetIds) {
|
||||||
// Album already contains that asset
|
// Album already contains that asset
|
||||||
if (album.assets?.some((a) => a.assetId === assetId)) {
|
if (album.assets?.some((a) => a.id === assetId)) {
|
||||||
alreadyExisting.push(assetId);
|
alreadyExisting.push(assetId);
|
||||||
continue;
|
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.
|
// Add album thumbnail if not exist.
|
||||||
if (!album.albumThumbnailAssetId && newRecords.length > 0) {
|
if (!album.albumThumbnailAssetId && album.assets.length > 0) {
|
||||||
album.albumThumbnailAssetId = newRecords[0].assetId;
|
album.albumThumbnailAssetId = album.assets[0].id;
|
||||||
await this.albumRepository.save(album);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.assetAlbumRepository.save([...newRecords]);
|
await this.albumRepository.save(album);
|
||||||
|
|
||||||
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
successfullyAdded: newRecords.length,
|
successfullyAdded: addAssetsDto.assetIds.length - alreadyExisting.length,
|
||||||
alreadyInAlbum: alreadyExisting,
|
alreadyInAlbum: alreadyExisting,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -330,19 +212,23 @@ export class AlbumRepository implements IAlbumRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number> {
|
async getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number> {
|
||||||
const result = await this.userAlbumRepository
|
return this.albumRepository.count({
|
||||||
.createQueryBuilder('usa')
|
where: [
|
||||||
.select('count(aa)', 'count')
|
{
|
||||||
.innerJoin('asset_album', 'aa', 'aa.albumId = usa.albumId')
|
ownerId: userId,
|
||||||
.innerJoin('albums', 'a', 'a.id = usa.albumId')
|
assets: {
|
||||||
.where('aa.assetId = :assetId', { assetId })
|
id: assetId,
|
||||||
.andWhere(
|
},
|
||||||
new Brackets((qb) => {
|
},
|
||||||
qb.where('a.ownerId = :userId', { userId }).orWhere('usa.sharedUserId = :userId', { userId });
|
{
|
||||||
}),
|
sharedUsers: {
|
||||||
)
|
id: userId,
|
||||||
.getRawOne();
|
},
|
||||||
|
assets: {
|
||||||
return result.count;
|
id: assetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { forwardRef, Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from './album.service';
|
||||||
import { AlbumController } from './album.controller';
|
import { AlbumController } from './album.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AlbumEntity, AssetAlbumEntity, UserAlbumEntity } from '@app/infra';
|
import { AlbumEntity } from '@app/infra';
|
||||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
|
import { AlbumRepository, IAlbumRepository } from './album-repository';
|
||||||
import { DownloadModule } from '../../modules/download/download.module';
|
import { DownloadModule } from '../../modules/download/download.module';
|
||||||
import { AssetModule } from '../asset/asset.module';
|
|
||||||
|
|
||||||
const ALBUM_REPOSITORY_PROVIDER = {
|
const ALBUM_REPOSITORY_PROVIDER = {
|
||||||
provide: IAlbumRepository,
|
provide: IAlbumRepository,
|
||||||
|
@ -13,11 +12,7 @@ const ALBUM_REPOSITORY_PROVIDER = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [TypeOrmModule.forFeature([AlbumEntity]), DownloadModule],
|
||||||
TypeOrmModule.forFeature([AlbumEntity, AssetAlbumEntity, UserAlbumEntity]),
|
|
||||||
DownloadModule,
|
|
||||||
forwardRef(() => AssetModule),
|
|
||||||
],
|
|
||||||
controllers: [AlbumController],
|
controllers: [AlbumController],
|
||||||
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
||||||
exports: [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 { IAlbumRepository } from './album-repository';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { ISharedLinkRepository } from '@app/domain';
|
import { ISharedLinkRepository } from '@app/domain';
|
||||||
import { newCryptoRepositoryMock, newSharedLinkRepositoryMock } from '@app/domain/../test';
|
import {
|
||||||
|
assetEntityStub,
|
||||||
|
newCryptoRepositoryMock,
|
||||||
|
newSharedLinkRepositoryMock,
|
||||||
|
userEntityStub,
|
||||||
|
} from '@app/domain/../test';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
|
@ -64,15 +69,8 @@ describe('Album service', () => {
|
||||||
albumEntity.albumThumbnailAssetId = null;
|
albumEntity.albumThumbnailAssetId = null;
|
||||||
albumEntity.sharedUsers = [
|
albumEntity.sharedUsers = [
|
||||||
{
|
{
|
||||||
id: '99',
|
...userEntityStub.user1,
|
||||||
albumId,
|
id: ownedAlbumSharedWithId,
|
||||||
sharedUserId: ownedAlbumSharedWithId,
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
albumInfo: {},
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
userInfo: {
|
|
||||||
id: ownedAlbumSharedWithId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -90,26 +88,12 @@ describe('Album service', () => {
|
||||||
albumEntity.albumThumbnailAssetId = null;
|
albumEntity.albumThumbnailAssetId = null;
|
||||||
albumEntity.sharedUsers = [
|
albumEntity.sharedUsers = [
|
||||||
{
|
{
|
||||||
id: '99',
|
...userEntityStub.user1,
|
||||||
albumId,
|
id: authUser.id,
|
||||||
sharedUserId: authUser.id,
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
albumInfo: {},
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
userInfo: {
|
|
||||||
id: authUser.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '98',
|
...userEntityStub.user1,
|
||||||
albumId,
|
id: sharedAlbumSharedAlsoWithId,
|
||||||
sharedUserId: sharedAlbumSharedAlsoWithId,
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
albumInfo: {},
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
userInfo: {
|
|
||||||
id: sharedAlbumSharedAlsoWithId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
albumEntity.sharedLinks = [];
|
albumEntity.sharedLinks = [];
|
||||||
|
@ -232,7 +216,7 @@ describe('Album service', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a not found exception if the album is not found', async () => {
|
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);
|
await expect(sut.getAlbumInfo(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -495,13 +479,8 @@ describe('Album service', () => {
|
||||||
albumEntity.sharedUsers = [];
|
albumEntity.sharedUsers = [];
|
||||||
albumEntity.assets = [
|
albumEntity.assets = [
|
||||||
{
|
{
|
||||||
id: '1',
|
...assetEntityStub.image,
|
||||||
albumId: '2',
|
id: '3',
|
||||||
assetId: '3',
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
albumInfo: {},
|
|
||||||
//@ts-expect-error Partial stub
|
|
||||||
assetInfo: {},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
albumEntity.albumThumbnailAssetId = null;
|
albumEntity.albumThumbnailAssetId = null;
|
||||||
|
@ -521,15 +500,7 @@ describe('Album service', () => {
|
||||||
|
|
||||||
albumEntity.albumThumbnailAssetId = 'nonexistent';
|
albumEntity.albumThumbnailAssetId = 'nonexistent';
|
||||||
assetEntity.id = newThumbnailAssetId;
|
assetEntity.id = newThumbnailAssetId;
|
||||||
albumEntity.assets = [
|
albumEntity.assets = [assetEntity];
|
||||||
{
|
|
||||||
id: '760841c1-f7c4-42b1-96af-c7d007a26126',
|
|
||||||
assetId: assetEntity.id,
|
|
||||||
albumId: albumEntity.id,
|
|
||||||
albumInfo: albumEntity,
|
|
||||||
assetInfo: assetEntity,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
albumRepositoryMock.getList.mockImplementation(async () => [albumEntity]);
|
albumRepositoryMock.getList.mockImplementation(async () => [albumEntity]);
|
||||||
albumRepositoryMock.updateAlbum.mockImplementation(async () => ({
|
albumRepositoryMock.updateAlbum.mockImplementation(async () => ({
|
||||||
...albumEntity,
|
...albumEntity,
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class AlbumService {
|
||||||
private shareCore: ShareCore;
|
private shareCore: ShareCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
|
@ -40,7 +40,7 @@ export class AlbumService {
|
||||||
albumId: string;
|
albumId: string;
|
||||||
validateIsOwner?: boolean;
|
validateIsOwner?: boolean;
|
||||||
}): Promise<AlbumEntity> {
|
}): Promise<AlbumEntity> {
|
||||||
const album = await this._albumRepository.get(albumId);
|
const album = await this.albumRepository.get(albumId);
|
||||||
if (!album) {
|
if (!album) {
|
||||||
throw new NotFoundException('Album Not Found');
|
throw new NotFoundException('Album Not Found');
|
||||||
}
|
}
|
||||||
|
@ -48,14 +48,14 @@ export class AlbumService {
|
||||||
|
|
||||||
if (validateIsOwner && !isOwner) {
|
if (validateIsOwner && !isOwner) {
|
||||||
throw new ForbiddenException('Unauthorized Album Access');
|
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');
|
throw new ForbiddenException('Unauthorized Album Access');
|
||||||
}
|
}
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(authUser: AuthUserDto, createAlbumDto: CreateAlbumDto): Promise<AlbumResponseDto> {
|
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);
|
return mapAlbum(albumEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,11 +68,11 @@ export class AlbumService {
|
||||||
let albums: AlbumEntity[];
|
let albums: AlbumEntity[];
|
||||||
|
|
||||||
if (typeof getAlbumsDto.assetId === 'string') {
|
if (typeof getAlbumsDto.assetId === 'string') {
|
||||||
albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
|
albums = await this.albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
|
||||||
} else {
|
} else {
|
||||||
albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
albums = await this.albumRepository.getList(authUser.id, getAlbumsDto);
|
||||||
if (getAlbumsDto.shared) {
|
if (getAlbumsDto.shared) {
|
||||||
const publicSharingAlbums = await this._albumRepository.getPublicSharingList(authUser.id);
|
const publicSharingAlbums = await this.albumRepository.getPublicSharingList(authUser.id);
|
||||||
albums = [...albums, ...publicSharingAlbums];
|
albums = [...albums, ...publicSharingAlbums];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ export class AlbumService {
|
||||||
|
|
||||||
async addUsersToAlbum(authUser: AuthUserDto, addUsersDto: AddUsersDto, albumId: string): Promise<AlbumResponseDto> {
|
async addUsersToAlbum(authUser: AuthUserDto, addUsersDto: AddUsersDto, albumId: string): Promise<AlbumResponseDto> {
|
||||||
const album = await this._getAlbum({ authUser, albumId });
|
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);
|
return mapAlbum(updatedAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export class AlbumService {
|
||||||
await this.shareCore.remove(authUser.id, sharedLink.id);
|
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> {
|
async removeUserFromAlbum(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
|
||||||
|
@ -116,7 +116,7 @@ export class AlbumService {
|
||||||
if (album.ownerId == sharedUserId) {
|
if (album.ownerId == sharedUserId) {
|
||||||
throw new BadRequestException('The owner of the album cannot be removed');
|
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(
|
async removeAssetsFromAlbum(
|
||||||
|
@ -125,7 +125,7 @@ export class AlbumService {
|
||||||
albumId: string,
|
albumId: string,
|
||||||
): Promise<AlbumResponseDto> {
|
): Promise<AlbumResponseDto> {
|
||||||
const album = await this._getAlbum({ authUser, albumId });
|
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 });
|
const newAlbum = await this._getAlbum({ authUser, albumId });
|
||||||
|
|
||||||
if (newAlbum) {
|
if (newAlbum) {
|
||||||
|
@ -150,7 +150,7 @@ export class AlbumService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
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 });
|
const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -170,17 +170,17 @@ export class AlbumService {
|
||||||
throw new BadRequestException('Unauthorized to change album info');
|
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);
|
return mapAlbum(updatedAlbum);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
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) {
|
async downloadArchive(authUser: AuthUserDto, albumId: string, dto: DownloadDto) {
|
||||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
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);
|
return this.downloadService.downloadArchive(album.albumName, assets);
|
||||||
}
|
}
|
||||||
|
@ -190,16 +190,16 @@ export class AlbumService {
|
||||||
|
|
||||||
// Check if the album's thumbnail is invalid by referencing
|
// Check if the album's thumbnail is invalid by referencing
|
||||||
// an asset outside the album.
|
// 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.
|
// Check if an empty album still has a thumbnail.
|
||||||
const isEmptyWithThumbnail = assets.length === 0 && album.albumThumbnailAssetId !== null;
|
const isEmptyWithThumbnail = assets.length === 0 && album.albumThumbnailAssetId !== null;
|
||||||
|
|
||||||
if (invalid || isEmptyWithThumbnail) {
|
if (invalid || isEmptyWithThumbnail) {
|
||||||
const albumThumbnailAssetId = assets[0]?.assetId;
|
const albumThumbnailAssetId = assets[0]?.id;
|
||||||
|
|
||||||
album.albumThumbnailAssetId = albumThumbnailAssetId || null;
|
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 { AssetService } from './asset.service';
|
||||||
import { AssetController } from './asset.controller';
|
import { AssetController } from './asset.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
@ -22,7 +22,7 @@ const ASSET_REPOSITORY_PROVIDER = {
|
||||||
DownloadModule,
|
DownloadModule,
|
||||||
TagModule,
|
TagModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
forwardRef(() => AlbumModule),
|
AlbumModule,
|
||||||
],
|
],
|
||||||
controllers: [AssetController],
|
controllers: [AssetController],
|
||||||
providers: [AssetService, ASSET_REPOSITORY_PROVIDER],
|
providers: [AssetService, ASSET_REPOSITORY_PROVIDER],
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
APIKeyUpdateDto,
|
APIKeyUpdateDto,
|
||||||
AuthUserDto,
|
AuthUserDto,
|
||||||
} from '@app/domain';
|
} 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 { ApiTags } from '@nestjs/swagger';
|
||||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||||
|
@ -31,21 +31,21 @@ export class APIKeyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@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);
|
return this.service.getById(authUser, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
updateKey(
|
updateKey(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id') id: string,
|
||||||
@Body(ValidationPipe) dto: APIKeyUpdateDto,
|
@Body(ValidationPipe) dto: APIKeyUpdateDto,
|
||||||
): Promise<APIKeyResponseDto> {
|
): Promise<APIKeyResponseDto> {
|
||||||
return this.service.update(authUser, id, dto);
|
return this.service.update(authUser, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@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);
|
return this.service.delete(authUser, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "number"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "number"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "number"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -2759,7 +2759,7 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
@ -21,11 +21,9 @@ export class AlbumResponseDto {
|
||||||
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
||||||
const sharedUsers: UserResponseDto[] = [];
|
const sharedUsers: UserResponseDto[] = [];
|
||||||
|
|
||||||
entity.sharedUsers?.forEach((userAlbum) => {
|
entity.sharedUsers?.forEach((user) => {
|
||||||
if (userAlbum.userInfo) {
|
const userDto = mapUser(user);
|
||||||
const user = mapUser(userAlbum.userInfo);
|
sharedUsers.push(userDto);
|
||||||
sharedUsers.push(user);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -38,7 +36,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
||||||
owner: mapUser(entity.owner),
|
owner: mapUser(entity.owner),
|
||||||
sharedUsers,
|
sharedUsers,
|
||||||
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
|
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,
|
assetCount: entity.assets?.length || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -46,11 +44,9 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
||||||
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
|
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
|
||||||
const sharedUsers: UserResponseDto[] = [];
|
const sharedUsers: UserResponseDto[] = [];
|
||||||
|
|
||||||
entity.sharedUsers?.forEach((userAlbum) => {
|
entity.sharedUsers?.forEach((user) => {
|
||||||
if (userAlbum.userInfo) {
|
const userDto = mapUser(user);
|
||||||
const user = mapUser(userAlbum.userInfo);
|
sharedUsers.push(userDto);
|
||||||
sharedUsers.push(user);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -4,13 +4,13 @@ export const IKeyRepository = 'IKeyRepository';
|
||||||
|
|
||||||
export interface IKeyRepository {
|
export interface IKeyRepository {
|
||||||
create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
|
create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
|
||||||
update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
|
update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
|
||||||
delete(userId: string, id: number): Promise<void>;
|
delete(userId: string, id: string): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Includes the hashed `key` for verification
|
* Includes the hashed `key` for verification
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
getKey(hashedToken: string): Promise<APIKeyEntity | null>;
|
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[]>;
|
getByUserId(userId: string): Promise<APIKeyEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,17 +47,19 @@ describe(APIKeyService.name, () => {
|
||||||
it('should throw an error if the key is not found', async () => {
|
it('should throw an error if the key is not found', async () => {
|
||||||
keyMock.getById.mockResolvedValue(null);
|
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 () => {
|
it('should update a key', async () => {
|
||||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
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 () => {
|
it('should throw an error if the key is not found', async () => {
|
||||||
keyMock.getById.mockResolvedValue(null);
|
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 () => {
|
it('should delete a key', async () => {
|
||||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
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 () => {
|
it('should throw an error if the key is not found', async () => {
|
||||||
keyMock.getById.mockResolvedValue(null);
|
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 () => {
|
it('should get a key by id', async () => {
|
||||||
keyMock.getById.mockResolvedValue(keyStub.admin);
|
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) };
|
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);
|
const exists = await this.repository.getById(authUser.id, id);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new BadRequestException('API Key not found');
|
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);
|
const exists = await this.repository.getById(authUser.id, id);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new BadRequestException('API Key not found');
|
throw new BadRequestException('API Key not found');
|
||||||
|
@ -44,7 +44,7 @@ export class APIKeyService {
|
||||||
await this.repository.delete(authUser.id, id);
|
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);
|
const key = await this.repository.getById(authUser.id, id);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new BadRequestException('API Key not found');
|
throw new BadRequestException('API Key not found');
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { APIKeyEntity } from '@app/infra/db/entities';
|
import { APIKeyEntity } from '@app/infra/db/entities';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export class APIKeyResponseDto {
|
export class APIKeyResponseDto {
|
||||||
@ApiProperty({ type: 'integer' })
|
id!: string;
|
||||||
id!: number;
|
|
||||||
name!: string;
|
name!: string;
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
updatedAt!: string;
|
updatedAt!: string;
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class SharedLinkResponseDto {
|
||||||
|
|
||||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||||
const linkAssets = sharedLink.assets || [];
|
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);
|
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
||||||
|
|
||||||
export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||||
const linkAssets = sharedLink.assets || [];
|
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);
|
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
APIKeyEntity,
|
APIKeyEntity,
|
||||||
|
AssetEntity,
|
||||||
AssetType,
|
AssetType,
|
||||||
SharedLinkEntity,
|
SharedLinkEntity,
|
||||||
SharedLinkType,
|
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 = {
|
const assetInfo: ExifResponseDto = {
|
||||||
id: 1,
|
id: 1,
|
||||||
make: 'camera-make',
|
make: 'camera-make',
|
||||||
|
@ -165,7 +190,7 @@ export const userTokenEntityStub = {
|
||||||
|
|
||||||
export const keyStub = {
|
export const keyStub = {
|
||||||
admin: Object.freeze({
|
admin: Object.freeze({
|
||||||
id: 1,
|
id: 'my-random-guid',
|
||||||
name: 'My Key',
|
name: 'My Key',
|
||||||
key: 'my-api-key (hashed)',
|
key: 'my-api-key (hashed)',
|
||||||
userId: authStub.admin.id,
|
userId: authStub.admin.id,
|
||||||
|
@ -348,66 +373,60 @@ export const sharedLinkStub = {
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
assets: [
|
assets: [
|
||||||
{
|
{
|
||||||
id: 'album-asset-123',
|
id: 'id_1',
|
||||||
albumId: 'album-123',
|
userId: 'user_id_1',
|
||||||
assetId: 'asset-123',
|
deviceAssetId: 'device_asset_id_1',
|
||||||
albumInfo: {} as any,
|
deviceId: 'device_id_1',
|
||||||
assetInfo: {
|
type: AssetType.VIDEO,
|
||||||
id: 'id_1',
|
originalPath: 'fake_path/jpeg',
|
||||||
userId: 'user_id_1',
|
resizePath: '',
|
||||||
deviceAssetId: 'device_asset_id_1',
|
createdAt: today.toISOString(),
|
||||||
deviceId: 'device_id_1',
|
modifiedAt: today.toISOString(),
|
||||||
type: AssetType.VIDEO,
|
updatedAt: today.toISOString(),
|
||||||
originalPath: 'fake_path/jpeg',
|
isFavorite: false,
|
||||||
resizePath: '',
|
mimeType: 'image/jpeg',
|
||||||
createdAt: today.toISOString(),
|
smartInfo: {
|
||||||
modifiedAt: today.toISOString(),
|
id: 'should-be-a-number',
|
||||||
updatedAt: today.toISOString(),
|
assetId: 'id_1',
|
||||||
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: '',
|
|
||||||
},
|
|
||||||
tags: [],
|
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,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { AssetAlbumEntity } from './asset-album.entity';
|
|
||||||
import { SharedLinkEntity } from './shared-link.entity';
|
import { SharedLinkEntity } from './shared-link.entity';
|
||||||
import { UserAlbumEntity } from './user-album.entity';
|
import { AssetEntity } from './asset.entity';
|
||||||
import { UserEntity } from './user.entity';
|
import { UserEntity } from './user.entity';
|
||||||
|
|
||||||
@Entity('albums')
|
@Entity('albums')
|
||||||
|
@ -17,12 +18,12 @@ export class AlbumEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||||
|
owner!: UserEntity;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, { eager: true })
|
|
||||||
owner!: UserEntity;
|
|
||||||
|
|
||||||
@Column({ default: 'Untitled Album' })
|
@Column({ default: 'Untitled Album' })
|
||||||
albumName!: string;
|
albumName!: string;
|
||||||
|
|
||||||
|
@ -35,11 +36,13 @@ export class AlbumEntity {
|
||||||
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
|
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
|
||||||
albumThumbnailAssetId!: string | null;
|
albumThumbnailAssetId!: string | null;
|
||||||
|
|
||||||
@OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
|
@ManyToMany(() => UserEntity, { eager: true })
|
||||||
sharedUsers?: UserAlbumEntity[];
|
@JoinTable()
|
||||||
|
sharedUsers!: UserEntity[];
|
||||||
|
|
||||||
@OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo)
|
@ManyToMany(() => AssetEntity, { eager: true })
|
||||||
assets?: AssetAlbumEntity[];
|
@JoinTable()
|
||||||
|
assets!: AssetEntity[];
|
||||||
|
|
||||||
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
||||||
sharedLinks!: SharedLinkEntity[];
|
sharedLinks!: SharedLinkEntity[];
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { UserEntity } from './user.entity';
|
||||||
|
|
||||||
@Entity('api_keys')
|
@Entity('api_keys')
|
||||||
export class APIKeyEntity {
|
export class APIKeyEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: number;
|
id!: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
@ -12,12 +12,12 @@ export class APIKeyEntity {
|
||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
key?: string;
|
key?: string;
|
||||||
|
|
||||||
@Column()
|
|
||||||
userId!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity)
|
@ManyToOne(() => UserEntity)
|
||||||
user?: UserEntity;
|
user?: UserEntity;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
@CreateDateColumn({ type: 'timestamptz' })
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
createdAt!: string;
|
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 './album.entity';
|
||||||
export * from './api-key.entity';
|
export * from './api-key.entity';
|
||||||
export * from './asset-album.entity';
|
|
||||||
export * from './asset.entity';
|
export * from './asset.entity';
|
||||||
export * from './device-info.entity';
|
export * from './device-info.entity';
|
||||||
export * from './exif.entity';
|
export * from './exif.entity';
|
||||||
export * from './smart-info.entity';
|
export * from './smart-info.entity';
|
||||||
export * from './system-config.entity';
|
export * from './system-config.entity';
|
||||||
export * from './tag.entity';
|
export * from './tag.entity';
|
||||||
export * from './user-album.entity';
|
|
||||||
export * from './user.entity';
|
export * from './user.entity';
|
||||||
export * from './user-token.entity';
|
export * from './user-token.entity';
|
||||||
export * from './shared-link.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);
|
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);
|
await this.repository.update({ userId, id }, dto);
|
||||||
return this.repository.findOneOrFail({ where: { id: dto.id } });
|
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 });
|
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 } });
|
return this.repository.findOne({ where: { userId, id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||||
},
|
},
|
||||||
album: {
|
album: {
|
||||||
assets: {
|
assets: {
|
||||||
assetInfo: {
|
exifInfo: true,
|
||||||
exifInfo: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -37,9 +35,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||||
},
|
},
|
||||||
album: {
|
album: {
|
||||||
assets: {
|
assets: {
|
||||||
assetInfo: {
|
createdAt: 'ASC',
|
||||||
createdAt: 'ASC',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -69,9 +65,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||||
relations: {
|
relations: {
|
||||||
assets: true,
|
assets: true,
|
||||||
album: {
|
album: {
|
||||||
assets: {
|
assets: true,
|
||||||
assetInfo: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
|
@ -109,7 +103,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||||
id,
|
id,
|
||||||
album: {
|
album: {
|
||||||
assets: {
|
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: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: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: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:typescript": "bash ./bin/generate-open-api.sh web",
|
||||||
"api:dart": "bash ./bin/generate-open-api.sh mobile",
|
"api:dart": "bash ./bin/generate-open-api.sh mobile",
|
||||||
"api:generate": "bash ./bin/generate-open-api.sh"
|
"api:generate": "bash ./bin/generate-open-api.sh"
|
||||||
|
|
Loading…
Reference in a new issue