mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
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:
@@ -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>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateTagDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
renameTagId?: string;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
|
||||
75
server/apps/immich/src/controllers/tag.controller.ts
Normal file
75
server/apps/immich/src/controllers/tag.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user