refactor(server): tags (#2589)

* refactor: tags

* chore: open api

* chore: unused import

* feat: add/remove/get tag assets

* chore: open api

* chore: finish tag tests for add/remove assets
This commit is contained in:
Jason Rasmussen
2023-05-31 21:51:28 -04:00
committed by GitHub
parent 631f13cf2f
commit 656dc08406
54 changed files with 2336 additions and 816 deletions

View File

@@ -1,7 +1,7 @@
import { SearchPropertiesDto } from './dto/search-properties.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/repository/Repository';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
@@ -12,7 +12,6 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { In } from 'typeorm/find-options/operator/In';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { ITagRepository } from '../tag/tag.repository';
import { IsNull, Not } from 'typeorm';
import { AssetSearchDto } from './dto/asset-search.dto';
@@ -52,10 +51,7 @@ export const IAssetRepository = 'IAssetRepository';
@Injectable()
export class AssetRepository implements IAssetRepository {
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@Inject(ITagRepository) private _tagRepository: ITagRepository,
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
) {}

View File

@@ -5,7 +5,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity, ExifEntity } from '@app/infra/entities';
import { AssetRepository, IAssetRepository } from './asset-repository';
import { DownloadModule } from '../../modules/download/download.module';
import { TagModule } from '../tag/tag.module';
import { AlbumModule } from '../album/album.module';
const ASSET_REPOSITORY_PROVIDER = {
@@ -18,7 +17,6 @@ const ASSET_REPOSITORY_PROVIDER = {
//
TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
DownloadModule,
TagModule,
AlbumModule,
],
controllers: [AssetController],

View File

@@ -1,14 +0,0 @@
import { TagType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class CreateTagDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsEnum(TagType)
@IsNotEmpty()
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
type!: TagType;
}

View File

@@ -1,11 +0,0 @@
import { IsOptional, IsString } from 'class-validator';
export class UpdateTagDto {
@IsString()
@IsOptional()
name?: string;
@IsString()
@IsOptional()
renameTagId?: string;
}

View File

@@ -1,49 +0,0 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, ValidationPipe } from '@nestjs/common';
import { TagService } from './tag.service';
import { CreateTagDto } from './dto/create-tag.dto';
import { UpdateTagDto } from './dto/update-tag.dto';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { ApiTags } from '@nestjs/swagger';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { mapTag, TagResponseDto } from '@app/domain';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
@ApiTags('Tag')
@Controller('tag')
@Authenticated()
export class TagController {
constructor(private readonly tagService: TagService) {}
@Post()
create(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) createTagDto: CreateTagDto,
): Promise<TagResponseDto> {
return this.tagService.create(authUser, createTagDto);
}
@Get()
findAll(@GetAuthUser() authUser: AuthUserDto): Promise<TagResponseDto[]> {
return this.tagService.findAll(authUser);
}
@Get(':id')
async findOne(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
const tag = await this.tagService.findOne(authUser, id);
return mapTag(tag);
}
@Patch(':id')
update(
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body(ValidationPipe) updateTagDto: UpdateTagDto,
): Promise<TagResponseDto> {
return this.tagService.update(authUser, id, updateTagDto);
}
@Delete(':id')
delete(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.tagService.remove(authUser, id);
}
}

View File

@@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { TagService } from './tag.service';
import { TagController } from './tag.controller';
import { TagEntity } from '@app/infra/entities';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TagRepository, ITagRepository } from './tag.repository';
const TAG_REPOSITORY_PROVIDER = {
provide: ITagRepository,
useClass: TagRepository,
};
@Module({
imports: [TypeOrmModule.forFeature([TagEntity])],
controllers: [TagController],
providers: [TagService, TAG_REPOSITORY_PROVIDER],
exports: [TAG_REPOSITORY_PROVIDER],
})
export class TagModule {}

View File

@@ -1,61 +0,0 @@
import { TagEntity, TagType } from '@app/infra/entities';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { UpdateTagDto } from './dto/update-tag.dto';
export interface ITagRepository {
create(userId: string, tagType: TagType, tagName: string): Promise<TagEntity>;
getByIds(userId: string, tagIds: string[]): Promise<TagEntity[]>;
getById(tagId: string, userId: string): Promise<TagEntity | null>;
getByUserId(userId: string): Promise<TagEntity[]>;
update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise<TagEntity | null>;
remove(tag: TagEntity): Promise<TagEntity>;
}
export const ITagRepository = 'ITagRepository';
@Injectable()
export class TagRepository implements ITagRepository {
constructor(
@InjectRepository(TagEntity)
private tagRepository: Repository<TagEntity>,
) {}
async create(userId: string, tagType: TagType, tagName: string): Promise<TagEntity> {
const tag = new TagEntity();
tag.name = tagName;
tag.type = tagType;
tag.userId = userId;
return this.tagRepository.save(tag);
}
async getById(tagId: string, userId: string): Promise<TagEntity | null> {
return await this.tagRepository.findOne({ where: { id: tagId, userId }, relations: ['user'] });
}
async getByIds(userId: string, tagIds: string[]): Promise<TagEntity[]> {
return await this.tagRepository.find({
where: { id: In(tagIds), userId },
relations: {
user: true,
},
});
}
async getByUserId(userId: string): Promise<TagEntity[]> {
return await this.tagRepository.find({ where: { userId } });
}
async update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise<TagEntity> {
tag.name = updateTagDto.name ?? tag.name;
tag.renameTagId = updateTagDto.renameTagId ?? tag.renameTagId;
return this.tagRepository.save(tag);
}
async remove(tag: TagEntity): Promise<TagEntity> {
return await this.tagRepository.remove(tag);
}
}

View File

@@ -1,94 +0,0 @@
import { TagEntity, TagType, UserEntity } from '@app/infra/entities';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { ITagRepository } from './tag.repository';
import { TagService } from './tag.service';
describe('TagService', () => {
let sut: TagService;
let tagRepositoryMock: jest.Mocked<ITagRepository>;
const user1AuthUser: AuthUserDto = Object.freeze({
id: '1111',
email: 'testuser@email.com',
isAdmin: false,
});
const user1: UserEntity = Object.freeze({
id: '1111',
firstName: 'Alex',
lastName: 'Tran',
isAdmin: true,
email: 'testuser@email.com',
profileImagePath: '',
shouldChangePassword: true,
createdAt: new Date('2022-12-02T19:29:23.603Z'),
deletedAt: null,
updatedAt: new Date('2022-12-02T19:29:23.603Z'),
tags: [],
assets: [],
oauthId: 'oauth-id-1',
storageLabel: null,
});
// const user2: UserEntity = Object.freeze({
// id: '2222',
// firstName: 'Alex',
// lastName: 'Tran',
// isAdmin: true,
// email: 'testuser2@email.com',
// profileImagePath: '',
// shouldChangePassword: true,
// createdAt: '2022-12-02T19:29:23.603Z',
// deletedAt: undefined,
// tags: [],
// oauthId: 'oauth-id-2',
// });
const user1Tag1: TagEntity = Object.freeze({
name: 'user 1 tag 1',
type: TagType.CUSTOM,
userId: user1.id,
user: user1,
renameTagId: '',
id: 'user1-tag-1-id',
assets: [],
});
// const user1Tag2: TagEntity = Object.freeze({
// name: 'user 1 tag 2',
// type: TagType.CUSTOM,
// userId: user1.id,
// user: user1,
// renameTagId: '',
// id: 'user1-tag-2-id',
// assets: [],
// });
beforeAll(() => {
tagRepositoryMock = {
create: jest.fn(),
getByIds: jest.fn(),
getById: jest.fn(),
getByUserId: jest.fn(),
remove: jest.fn(),
update: jest.fn(),
};
sut = new TagService(tagRepositoryMock);
});
it('creates tag', async () => {
const createTagDto = {
name: 'user 1 tag 1',
type: TagType.CUSTOM,
};
tagRepositoryMock.create.mockResolvedValue(user1Tag1);
const result = await sut.create(user1AuthUser, createTagDto);
expect(result.userId).toEqual(user1AuthUser.id);
expect(result.name).toEqual(createTagDto.name);
expect(result.type).toEqual(createTagDto.type);
});
});

View File

@@ -1,52 +0,0 @@
import { TagEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateTagDto } from './dto/create-tag.dto';
import { UpdateTagDto } from './dto/update-tag.dto';
import { ITagRepository } from './tag.repository';
import { mapTag, TagResponseDto } from '@app/domain';
@Injectable()
export class TagService {
readonly logger = new Logger(TagService.name);
constructor(@Inject(ITagRepository) private _tagRepository: ITagRepository) {}
async create(authUser: AuthUserDto, createTagDto: CreateTagDto) {
try {
const newTag = await this._tagRepository.create(authUser.id, createTagDto.type, createTagDto.name);
return mapTag(newTag);
} catch (e: any) {
this.logger.error(e, e.stack);
throw new BadRequestException(`Failed to create tag: ${e.detail}`);
}
}
async findAll(authUser: AuthUserDto) {
const tags = await this._tagRepository.getByUserId(authUser.id);
return tags.map(mapTag);
}
async findOne(authUser: AuthUserDto, id: string): Promise<TagEntity> {
const tag = await this._tagRepository.getById(id, authUser.id);
if (!tag) {
throw new BadRequestException('Tag not found');
}
return tag;
}
async update(authUser: AuthUserDto, id: string, updateTagDto: UpdateTagDto): Promise<TagResponseDto> {
const tag = await this.findOne(authUser, id);
await this._tagRepository.update(tag, updateTagDto);
return mapTag(tag);
}
async remove(authUser: AuthUserDto, id: string): Promise<void> {
const tag = await this.findOne(authUser, id);
await this._tagRepository.remove(tag);
}
}

View File

@@ -3,7 +3,6 @@ import { AssetModule } from './api-v1/asset/asset.module';
import { AlbumModule } from './api-v1/album/album.module';
import { AppController } from './app.controller';
import { ScheduleModule } from '@nestjs/schedule';
import { TagModule } from './api-v1/tag/tag.module';
import { DomainModule, SearchService } from '@app/domain';
import { InfraModule } from '@app/infra';
import {
@@ -20,6 +19,7 @@ import {
SharedLinkController,
SystemConfigController,
UserController,
TagController,
} from './controllers';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './middlewares/auth.guard';
@@ -27,11 +27,11 @@ import { AppCronJobs } from './app.cron-jobs';
@Module({
imports: [
//
DomainModule.register({ imports: [InfraModule] }),
AssetModule,
AlbumModule,
ScheduleModule.forRoot(),
TagModule,
],
controllers: [
AppController,
@@ -46,6 +46,7 @@ import { AppCronJobs } from './app.cron-jobs';
ServerInfoController,
SharedLinkController,
SystemConfigController,
TagController,
UserController,
PersonController,
],

View File

@@ -10,4 +10,5 @@ export * from './search.controller';
export * from './server-info.controller';
export * from './shared-link.controller';
export * from './system-config.controller';
export * from './tag.controller';
export * from './user.controller';

View File

@@ -0,0 +1,75 @@
import {
AssetIdsDto,
AssetIdsResponseDto,
AssetResponseDto,
CreateTagDto,
TagResponseDto,
TagService,
UpdateTagDto,
} from '@app/domain';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUserDto, GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
import { UseValidation } from '../decorators/use-validation.decorator';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('Tag')
@Controller('tag')
@Authenticated()
@UseValidation()
export class TagController {
constructor(private service: TagService) {}
@Post()
createTag(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
return this.service.create(authUser, dto);
}
@Get()
getAllTags(@GetAuthUser() authUser: AuthUserDto): Promise<TagResponseDto[]> {
return this.service.getAll(authUser);
}
@Get(':id')
getTagById(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.getById(authUser, id);
}
@Patch(':id')
updateTag(
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateTagDto,
): Promise<TagResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
deleteTag(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);
}
@Get(':id/assets')
getTagAssets(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(authUser, id);
}
@Put(':id/assets')
tagAssets(
@GetAuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.addAssets(authUser, id, dto);
}
@Delete(':id/assets')
untagAssets(
@GetAuthUser() authUser: AuthUserDto,
@Body() dto: AssetIdsDto,
@Param() { id }: UUIDParamDto,
): Promise<AssetIdsResponseDto[]> {
return this.service.removeAssets(authUser, id, dto);
}
}