diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index eafeb6851..9335f934b 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -25,6 +25,8 @@ doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucketResponseDto.md doc/AssetCountByUserIdResponseDto.md doc/AssetFileUploadResponseDto.md +doc/AssetIdsDto.md +doc/AssetIdsResponseDto.md doc/AssetResponseDto.md doc/AssetTypeEnum.md doc/AuthDeviceResponseDto.md @@ -154,6 +156,8 @@ lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart lib/model/asset_count_by_user_id_response_dto.dart lib/model/asset_file_upload_response_dto.dart +lib/model/asset_ids_dto.dart +lib/model/asset_ids_response_dto.dart lib/model/asset_response_dto.dart lib/model/asset_type_enum.dart lib/model/auth_device_response_dto.dart @@ -252,6 +256,8 @@ test/asset_count_by_time_bucket_response_dto_test.dart test/asset_count_by_time_bucket_test.dart test/asset_count_by_user_id_response_dto_test.dart test/asset_file_upload_response_dto_test.dart +test/asset_ids_dto_test.dart +test/asset_ids_response_dto_test.dart test/asset_response_dto_test.dart test/asset_type_enum_test.dart test/auth_device_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index c0b851126..99d6f2da2 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AssetIdsDto.md b/mobile/openapi/doc/AssetIdsDto.md new file mode 100644 index 000000000..002c3ee5e Binary files /dev/null and b/mobile/openapi/doc/AssetIdsDto.md differ diff --git a/mobile/openapi/doc/AssetIdsResponseDto.md b/mobile/openapi/doc/AssetIdsResponseDto.md new file mode 100644 index 000000000..4aaebf73a Binary files /dev/null and b/mobile/openapi/doc/AssetIdsResponseDto.md differ diff --git a/mobile/openapi/doc/TagApi.md b/mobile/openapi/doc/TagApi.md index 3dd11aab6..7c5b3d9a1 100644 Binary files a/mobile/openapi/doc/TagApi.md and b/mobile/openapi/doc/TagApi.md differ diff --git a/mobile/openapi/doc/TagResponseDto.md b/mobile/openapi/doc/TagResponseDto.md index 31805c2a8..366e95033 100644 Binary files a/mobile/openapi/doc/TagResponseDto.md and b/mobile/openapi/doc/TagResponseDto.md differ diff --git a/mobile/openapi/doc/UpdateTagDto.md b/mobile/openapi/doc/UpdateTagDto.md index ad4e551be..42aae30b5 100644 Binary files a/mobile/openapi/doc/UpdateTagDto.md and b/mobile/openapi/doc/UpdateTagDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 833628d62..20f225513 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/tag_api.dart b/mobile/openapi/lib/api/tag_api.dart index 3591cf11c..e420792ca 100644 Binary files a/mobile/openapi/lib/api/tag_api.dart and b/mobile/openapi/lib/api/tag_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7b46ea15f..d3f079074 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/asset_ids_dto.dart b/mobile/openapi/lib/model/asset_ids_dto.dart new file mode 100644 index 000000000..d76462847 Binary files /dev/null and b/mobile/openapi/lib/model/asset_ids_dto.dart differ diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart new file mode 100644 index 000000000..6f9befea7 Binary files /dev/null and b/mobile/openapi/lib/model/asset_ids_response_dto.dart differ diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index 42c386df3..8c4dd45ed 100644 Binary files a/mobile/openapi/lib/model/tag_response_dto.dart and b/mobile/openapi/lib/model/tag_response_dto.dart differ diff --git a/mobile/openapi/lib/model/update_tag_dto.dart b/mobile/openapi/lib/model/update_tag_dto.dart index 85c1761a6..325131480 100644 Binary files a/mobile/openapi/lib/model/update_tag_dto.dart and b/mobile/openapi/lib/model/update_tag_dto.dart differ diff --git a/mobile/openapi/test/asset_ids_dto_test.dart b/mobile/openapi/test/asset_ids_dto_test.dart new file mode 100644 index 000000000..840f6f5cc Binary files /dev/null and b/mobile/openapi/test/asset_ids_dto_test.dart differ diff --git a/mobile/openapi/test/asset_ids_response_dto_test.dart b/mobile/openapi/test/asset_ids_response_dto_test.dart new file mode 100644 index 000000000..df6a8642d Binary files /dev/null and b/mobile/openapi/test/asset_ids_response_dto_test.dart differ diff --git a/mobile/openapi/test/tag_api_test.dart b/mobile/openapi/test/tag_api_test.dart index a504aedfa..1b4d79744 100644 Binary files a/mobile/openapi/test/tag_api_test.dart and b/mobile/openapi/test/tag_api_test.dart differ diff --git a/mobile/openapi/test/tag_response_dto_test.dart b/mobile/openapi/test/tag_response_dto_test.dart index 41b581efb..705a21ecd 100644 Binary files a/mobile/openapi/test/tag_response_dto_test.dart and b/mobile/openapi/test/tag_response_dto_test.dart differ diff --git a/mobile/openapi/test/update_tag_dto_test.dart b/mobile/openapi/test/update_tag_dto_test.dart index 4f4e9498b..7c67e55d7 100644 Binary files a/mobile/openapi/test/update_tag_dto_test.dart and b/mobile/openapi/test/update_tag_dto_test.dart differ diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 982e5c9f5..de7a9df9e 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -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, - - @Inject(ITagRepository) private _tagRepository: ITagRepository, + @InjectRepository(AssetEntity) private assetRepository: Repository, @InjectRepository(ExifEntity) private exifRepository: Repository, ) {} diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts index ebadeca75..6ec9c603a 100644 --- a/server/apps/immich/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -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], diff --git a/server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts b/server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts deleted file mode 100644 index 64632f1f6..000000000 --- a/server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsOptional, IsString } from 'class-validator'; - -export class UpdateTagDto { - @IsString() - @IsOptional() - name?: string; - - @IsString() - @IsOptional() - renameTagId?: string; -} diff --git a/server/apps/immich/src/api-v1/tag/tag.controller.ts b/server/apps/immich/src/api-v1/tag/tag.controller.ts deleted file mode 100644 index df12513df..000000000 --- a/server/apps/immich/src/api-v1/tag/tag.controller.ts +++ /dev/null @@ -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 { - return this.tagService.create(authUser, createTagDto); - } - - @Get() - findAll(@GetAuthUser() authUser: AuthUserDto): Promise { - return this.tagService.findAll(authUser); - } - - @Get(':id') - async findOne(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - 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 { - return this.tagService.update(authUser, id, updateTagDto); - } - - @Delete(':id') - delete(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.tagService.remove(authUser, id); - } -} diff --git a/server/apps/immich/src/api-v1/tag/tag.module.ts b/server/apps/immich/src/api-v1/tag/tag.module.ts deleted file mode 100644 index 38b7924a6..000000000 --- a/server/apps/immich/src/api-v1/tag/tag.module.ts +++ /dev/null @@ -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 {} diff --git a/server/apps/immich/src/api-v1/tag/tag.repository.ts b/server/apps/immich/src/api-v1/tag/tag.repository.ts deleted file mode 100644 index ec32d8b74..000000000 --- a/server/apps/immich/src/api-v1/tag/tag.repository.ts +++ /dev/null @@ -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; - getByIds(userId: string, tagIds: string[]): Promise; - getById(tagId: string, userId: string): Promise; - getByUserId(userId: string): Promise; - update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise; - remove(tag: TagEntity): Promise; -} - -export const ITagRepository = 'ITagRepository'; - -@Injectable() -export class TagRepository implements ITagRepository { - constructor( - @InjectRepository(TagEntity) - private tagRepository: Repository, - ) {} - - async create(userId: string, tagType: TagType, tagName: string): Promise { - 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 { - return await this.tagRepository.findOne({ where: { id: tagId, userId }, relations: ['user'] }); - } - - async getByIds(userId: string, tagIds: string[]): Promise { - return await this.tagRepository.find({ - where: { id: In(tagIds), userId }, - relations: { - user: true, - }, - }); - } - - async getByUserId(userId: string): Promise { - return await this.tagRepository.find({ where: { userId } }); - } - - async update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise { - tag.name = updateTagDto.name ?? tag.name; - tag.renameTagId = updateTagDto.renameTagId ?? tag.renameTagId; - - return this.tagRepository.save(tag); - } - - async remove(tag: TagEntity): Promise { - return await this.tagRepository.remove(tag); - } -} diff --git a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts deleted file mode 100644 index 4609bb781..000000000 --- a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts +++ /dev/null @@ -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; - - 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); - }); -}); diff --git a/server/apps/immich/src/api-v1/tag/tag.service.ts b/server/apps/immich/src/api-v1/tag/tag.service.ts deleted file mode 100644 index 71998661a..000000000 --- a/server/apps/immich/src/api-v1/tag/tag.service.ts +++ /dev/null @@ -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 { - 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 { - const tag = await this.findOne(authUser, id); - - await this._tagRepository.update(tag, updateTagDto); - - return mapTag(tag); - } - - async remove(authUser: AuthUserDto, id: string): Promise { - const tag = await this.findOne(authUser, id); - await this._tagRepository.remove(tag); - } -} diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index a9168975b..51f5e3207 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -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, ], diff --git a/server/apps/immich/src/controllers/index.ts b/server/apps/immich/src/controllers/index.ts index d86db2beb..5917f0f79 100644 --- a/server/apps/immich/src/controllers/index.ts +++ b/server/apps/immich/src/controllers/index.ts @@ -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'; diff --git a/server/apps/immich/src/controllers/tag.controller.ts b/server/apps/immich/src/controllers/tag.controller.ts new file mode 100644 index 000000000..45b5233e2 --- /dev/null +++ b/server/apps/immich/src/controllers/tag.controller.ts @@ -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 { + return this.service.create(authUser, dto); + } + + @Get() + getAllTags(@GetAuthUser() authUser: AuthUserDto): Promise { + return this.service.getAll(authUser); + } + + @Get(':id') + getTagById(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getById(authUser, id); + } + + @Patch(':id') + updateTag( + @GetAuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: UpdateTagDto, + ): Promise { + return this.service.update(authUser, id, dto); + } + + @Delete(':id') + deleteTag(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.remove(authUser, id); + } + + @Get(':id/assets') + getTagAssets(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getAssets(authUser, id); + } + + @Put(':id/assets') + tagAssets( + @GetAuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetIdsDto, + ): Promise { + return this.service.addAssets(authUser, id, dto); + } + + @Delete(':id/assets') + untagAssets( + @GetAuthUser() authUser: AuthUserDto, + @Body() dto: AssetIdsDto, + @Param() { id }: UUIDParamDto, + ): Promise { + return this.service.removeAssets(authUser, id, dto); + } +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 45a6dcde3..a09c0e493 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1785,6 +1785,357 @@ ] } }, + "/tag": { + "post": { + "operationId": "createTag", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTagDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResponseDto" + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "get": { + "operationId": "getAllTags", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagResponseDto" + } + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, + "/tag/{id}": { + "get": { + "operationId": "getTagById", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResponseDto" + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "patch": { + "operationId": "updateTag", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTagDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagResponseDto" + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "delete": { + "operationId": "deleteTag", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, + "/tag/{id}/assets": { + "get": { + "operationId": "getTagAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + } + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "put": { + "operationId": "tagAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetIdsDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetIdsResponseDto" + } + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + }, + "delete": { + "operationId": "untagAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetIdsDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssetIdsResponseDto" + } + } + } + } + } + }, + "tags": [ + "Tag" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, "/user": { "get": { "operationId": "getAllUsers", @@ -3598,206 +3949,6 @@ ] } }, - "/tag": { - "post": { - "operationId": "create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTagDto" - } - } - } - }, - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TagResponseDto" - } - } - } - } - }, - "tags": [ - "Tag" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - }, - "get": { - "operationId": "findAll", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TagResponseDto" - } - } - } - } - } - }, - "tags": [ - "Tag" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, - "/tag/{id}": { - "get": { - "operationId": "findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TagResponseDto" - } - } - } - } - }, - "tags": [ - "Tag" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - }, - "patch": { - "operationId": "update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTagDto" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TagResponseDto" - } - } - } - } - }, - "tags": [ - "Tag" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - }, - "delete": { - "operationId": "delete", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Tag" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, "/album/count-by-user-id": { "get": { "operationId": "getAlbumCountByUserId", @@ -4384,26 +4535,22 @@ "TagResponseDto": { "type": "object", "properties": { - "id": { - "type": "string" - }, "type": { "$ref": "#/components/schemas/TagTypeEnum" }, + "id": { + "type": "string" + }, "name": { "type": "string" }, "userId": { "type": "string" - }, - "renameTagId": { - "type": "string", - "nullable": true } }, "required": [ - "id", "type", + "id", "name", "userId" ] @@ -5665,6 +5812,67 @@ "presetOptions" ] }, + "CreateTagDto": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/TagTypeEnum" + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "name" + ] + }, + "UpdateTagDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "AssetIdsDto": { + "type": "object", + "properties": { + "assetIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + "required": [ + "assetIds" + ] + }, + "AssetIdsResponseDto": { + "type": "object", + "properties": { + "assetId": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "error": { + "enum": [ + "duplicate", + "no_permission", + "not_found" + ], + "type": "string" + } + }, + "required": [ + "assetId", + "success" + ] + }, "CreateUserDto": { "type": "object", "properties": { @@ -6318,32 +6526,6 @@ "assetIds" ] }, - "CreateTagDto": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/TagTypeEnum" - }, - "name": { - "type": "string" - } - }, - "required": [ - "type", - "name" - ] - }, - "UpdateTagDto": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "renameTagId": { - "type": "string" - } - } - }, "AlbumCountResponseDto": { "type": "object", "properties": { diff --git a/server/libs/domain/src/asset/dto/asset-ids.dto.ts b/server/libs/domain/src/asset/dto/asset-ids.dto.ts new file mode 100644 index 000000000..8c9b60b3d --- /dev/null +++ b/server/libs/domain/src/asset/dto/asset-ids.dto.ts @@ -0,0 +1,6 @@ +import { ValidateUUID } from '../../../../../apps/immich/src/decorators/validate-uuid.decorator'; + +export class AssetIdsDto { + @ValidateUUID({ each: true }) + assetIds!: string[]; +} diff --git a/server/libs/domain/src/asset/dto/index.ts b/server/libs/domain/src/asset/dto/index.ts new file mode 100644 index 000000000..900b25d28 --- /dev/null +++ b/server/libs/domain/src/asset/dto/index.ts @@ -0,0 +1,2 @@ +export * from './asset-ids.dto'; +export * from './map-marker.dto'; diff --git a/server/libs/domain/src/asset/dto/map-marker.dto.ts b/server/libs/domain/src/asset/dto/map-marker.dto.ts index 5398b9547..af39a4980 100644 --- a/server/libs/domain/src/asset/dto/map-marker.dto.ts +++ b/server/libs/domain/src/asset/dto/map-marker.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { toBoolean } from 'apps/immich/src/utils/transform.util'; import { Transform, Type } from 'class-transformer'; import { IsBoolean, IsDate, IsOptional } from 'class-validator'; +import { toBoolean } from '../../../../../apps/immich/src/utils/transform.util'; export class MapMarkerDto { @ApiProperty() diff --git a/server/libs/domain/src/asset/index.ts b/server/libs/domain/src/asset/index.ts index aa429787d..833df4c56 100644 --- a/server/libs/domain/src/asset/index.ts +++ b/server/libs/domain/src/asset/index.ts @@ -1,3 +1,4 @@ export * from './asset.repository'; export * from './asset.service'; +export * from './dto'; export * from './response-dto'; diff --git a/server/libs/domain/src/asset/response-dto/asset-ids-response.dto.ts b/server/libs/domain/src/asset/response-dto/asset-ids-response.dto.ts new file mode 100644 index 000000000..928bed24d --- /dev/null +++ b/server/libs/domain/src/asset/response-dto/asset-ids-response.dto.ts @@ -0,0 +1,11 @@ +export enum AssetIdErrorReason { + DUPLICATE = 'duplicate', + NO_PERMISSION = 'no_permission', + NOT_FOUND = 'not_found', +} + +export class AssetIdsResponseDto { + assetId!: string; + success!: boolean; + error?: AssetIdErrorReason; +} diff --git a/server/libs/domain/src/asset/response-dto/index.ts b/server/libs/domain/src/asset/response-dto/index.ts index 7e17e324f..b82249d2b 100644 --- a/server/libs/domain/src/asset/response-dto/index.ts +++ b/server/libs/domain/src/asset/response-dto/index.ts @@ -1,4 +1,5 @@ +export * from './asset-ids-response.dto'; export * from './asset-response.dto'; export * from './exif-response.dto'; -export * from './smart-info-response.dto'; export * from './map-marker-response.dto'; +export * from './smart-info-response.dto'; diff --git a/server/libs/domain/src/domain.module.ts b/server/libs/domain/src/domain.module.ts index 9aea15b44..ec2443cd7 100644 --- a/server/libs/domain/src/domain.module.ts +++ b/server/libs/domain/src/domain.module.ts @@ -17,6 +17,7 @@ import { SmartInfoService } from './smart-info'; import { StorageService } from './storage'; import { StorageTemplateService } from './storage-template'; import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config'; +import { TagService } from './tag'; import { UserService } from './user'; const providers: Provider[] = [ @@ -38,6 +39,7 @@ const providers: Provider[] = [ StorageService, StorageTemplateService, SystemConfigService, + TagService, UserService, { provide: INITIAL_SYSTEM_CONFIG, diff --git a/server/libs/domain/src/tag/index.ts b/server/libs/domain/src/tag/index.ts index 0b415ca92..0ea02172e 100644 --- a/server/libs/domain/src/tag/index.ts +++ b/server/libs/domain/src/tag/index.ts @@ -1 +1,4 @@ -export * from './response-dto'; +export * from './tag-response.dto'; +export * from './tag.dto'; +export * from './tag.repository'; +export * from './tag.service'; diff --git a/server/libs/domain/src/tag/response-dto/index.ts b/server/libs/domain/src/tag/response-dto/index.ts deleted file mode 100644 index b08b1f61c..000000000 --- a/server/libs/domain/src/tag/response-dto/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tag-response.dto'; diff --git a/server/libs/domain/src/tag/response-dto/tag-response.dto.ts b/server/libs/domain/src/tag/tag-response.dto.ts similarity index 83% rename from server/libs/domain/src/tag/response-dto/tag-response.dto.ts rename to server/libs/domain/src/tag/tag-response.dto.ts index b679a378c..a533b15c9 100644 --- a/server/libs/domain/src/tag/response-dto/tag-response.dto.ts +++ b/server/libs/domain/src/tag/tag-response.dto.ts @@ -2,17 +2,11 @@ import { TagEntity, TagType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; export class TagResponseDto { - @ApiProperty() id!: string; - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) type!: string; - name!: string; - userId!: string; - - renameTagId?: string | null; } export function mapTag(entity: TagEntity): TagResponseDto { @@ -21,6 +15,5 @@ export function mapTag(entity: TagEntity): TagResponseDto { type: entity.type, name: entity.name, userId: entity.userId, - renameTagId: entity.renameTagId, }; } diff --git a/server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts b/server/libs/domain/src/tag/tag.dto.ts similarity index 64% rename from server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts rename to server/libs/domain/src/tag/tag.dto.ts index 69dd946eb..4cce47530 100644 --- a/server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts +++ b/server/libs/domain/src/tag/tag.dto.ts @@ -1,6 +1,6 @@ import { TagType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateTagDto { @IsString() @@ -12,3 +12,9 @@ export class CreateTagDto { @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) type!: TagType; } + +export class UpdateTagDto { + @IsString() + @IsOptional() + name?: string; +} diff --git a/server/libs/domain/src/tag/tag.repository.ts b/server/libs/domain/src/tag/tag.repository.ts new file mode 100644 index 000000000..4e6f583b4 --- /dev/null +++ b/server/libs/domain/src/tag/tag.repository.ts @@ -0,0 +1,16 @@ +import { AssetEntity, TagEntity } from '@app/infra/entities'; + +export const ITagRepository = 'ITagRepository'; + +export interface ITagRepository { + getById(userId: string, tagId: string): Promise; + getAll(userId: string): Promise; + create(tag: Partial): Promise; + update(tag: Partial): Promise; + remove(tag: TagEntity): Promise; + hasName(userId: string, name: string): Promise; + hasAsset(userId: string, tagId: string, assetId: string): Promise; + getAssets(userId: string, tagId: string): Promise; + addAssets(userId: string, tagId: string, assetIds: string[]): Promise; + removeAssets(userId: string, tagId: string, assetIds: string[]): Promise; +} diff --git a/server/libs/domain/src/tag/tag.service.spec.ts b/server/libs/domain/src/tag/tag.service.spec.ts new file mode 100644 index 000000000..62c145080 --- /dev/null +++ b/server/libs/domain/src/tag/tag.service.spec.ts @@ -0,0 +1,178 @@ +import { TagType } from '@app/infra/entities'; +import { BadRequestException } from '@nestjs/common'; +import { when } from 'jest-when'; +import { assetEntityStub, authStub, newTagRepositoryMock, tagResponseStub, tagStub } from '../../test'; +import { AssetIdErrorReason } from '../asset'; +import { ITagRepository } from './tag.repository'; +import { TagService } from './tag.service'; + +describe(TagService.name, () => { + let sut: TagService; + let tagMock: jest.Mocked; + + beforeEach(() => { + tagMock = newTagRepositoryMock(); + sut = new TagService(tagMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('getAll', () => { + it('should return all tags for a user', async () => { + tagMock.getAll.mockResolvedValue([tagStub.tag1]); + await expect(sut.getAll(authStub.admin)).resolves.toEqual([tagResponseStub.tag1]); + expect(tagMock.getAll).toHaveBeenCalledWith(authStub.admin.id); + }); + }); + + describe('getById', () => { + it('should throw an error for an invalid id', async () => { + tagMock.getById.mockResolvedValue(null); + await expect(sut.getById(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + }); + + it('should return a tag for a user', async () => { + tagMock.getById.mockResolvedValue(tagStub.tag1); + await expect(sut.getById(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + }); + }); + + describe('create', () => { + it('should throw an error for a duplicate tag', async () => { + tagMock.hasName.mockResolvedValue(true); + await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.create).not.toHaveBeenCalled(); + }); + + it('should create a new tag', async () => { + tagMock.create.mockResolvedValue(tagStub.tag1); + await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).resolves.toEqual( + tagResponseStub.tag1, + ); + expect(tagMock.create).toHaveBeenCalledWith({ + userId: authStub.admin.id, + name: 'tag-1', + type: TagType.CUSTOM, + }); + }); + }); + + describe('update', () => { + it('should throw an error for an invalid id', async () => { + tagMock.getById.mockResolvedValue(null); + await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.remove).not.toHaveBeenCalled(); + }); + + it('should update a tag', async () => { + tagMock.getById.mockResolvedValue(tagStub.tag1); + tagMock.update.mockResolvedValue(tagStub.tag1); + await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).resolves.toEqual(tagResponseStub.tag1); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', name: 'tag-2' }); + }); + }); + + describe('remove', () => { + it('should throw an error for an invalid id', async () => { + tagMock.getById.mockResolvedValue(null); + await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.remove).not.toHaveBeenCalled(); + }); + + it('should remove a tag', async () => { + tagMock.getById.mockResolvedValue(tagStub.tag1); + await sut.remove(authStub.admin, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.remove).toHaveBeenCalledWith(tagStub.tag1); + }); + }); + + describe('getAssets', () => { + it('should throw an error for an invalid id', async () => { + tagMock.getById.mockResolvedValue(null); + await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.remove).not.toHaveBeenCalled(); + }); + + it('should get the assets for a tag', async () => { + tagMock.getById.mockResolvedValue(tagStub.tag1); + tagMock.getAssets.mockResolvedValue([assetEntityStub.image]); + await sut.getAssets(authStub.admin, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + }); + }); + + describe('addAssets', () => { + it('should throw an error for an invalid id', async () => { + tagMock.getById.mockResolvedValue(null); + await expect(sut.addAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.addAssets).not.toHaveBeenCalled(); + }); + + it('should reject duplicate asset ids and accept new ones', async () => { + tagMock.getById.mockResolvedValue(tagStub.tag1); + + when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true); + when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-2').mockResolvedValue(false); + + await expect( + sut.addAssets(authStub.admin, 'tag-1', { + assetIds: ['asset-1', 'asset-2'], + }), + ).resolves.toEqual([ + { assetId: 'asset-1', success: false, error: AssetIdErrorReason.DUPLICATE }, + { assetId: 'asset-2', success: true }, + ]); + + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); + expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1', ['asset-2']); + }); + }); + + describe('removeAssets', () => { + it('should throw an error for an invalid id', async () => { + tagMock.getById.mockResolvedValue(null); + await expect(sut.removeAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.removeAssets).not.toHaveBeenCalled(); + }); + + it('should accept accept ids that are tagged and reject the rest', async () => { + tagMock.getById.mockResolvedValue(tagStub.tag1); + + when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true); + when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-2').mockResolvedValue(false); + + await expect( + sut.removeAssets(authStub.admin, 'tag-1', { + assetIds: ['asset-1', 'asset-2'], + }), + ).resolves.toEqual([ + { assetId: 'asset-1', success: true }, + { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, + ]); + + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); + expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1', ['asset-1']); + }); + }); +}); diff --git a/server/libs/domain/src/tag/tag.service.ts b/server/libs/domain/src/tag/tag.service.ts new file mode 100644 index 000000000..25075bd4e --- /dev/null +++ b/server/libs/domain/src/tag/tag.service.ts @@ -0,0 +1,104 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, mapAsset } from '../asset'; +import { AuthUserDto } from '../auth'; +import { mapTag, TagResponseDto } from './tag-response.dto'; +import { CreateTagDto, UpdateTagDto } from './tag.dto'; +import { ITagRepository } from './tag.repository'; + +@Injectable() +export class TagService { + constructor(@Inject(ITagRepository) private repository: ITagRepository) {} + + getAll(authUser: AuthUserDto) { + return this.repository.getAll(authUser.id).then((tags) => tags.map(mapTag)); + } + + async getById(authUser: AuthUserDto, id: string): Promise { + const tag = await this.findOrFail(authUser, id); + return mapTag(tag); + } + + async create(authUser: AuthUserDto, dto: CreateTagDto) { + const duplicate = await this.repository.hasName(authUser.id, dto.name); + if (duplicate) { + throw new BadRequestException(`A tag with that name already exists`); + } + + const tag = await this.repository.create({ + userId: authUser.id, + name: dto.name, + type: dto.type, + }); + + return mapTag(tag); + } + + async update(authUser: AuthUserDto, id: string, dto: UpdateTagDto): Promise { + await this.findOrFail(authUser, id); + const tag = await this.repository.update({ id, name: dto.name }); + return mapTag(tag); + } + + async remove(authUser: AuthUserDto, id: string): Promise { + const tag = await this.findOrFail(authUser, id); + await this.repository.remove(tag); + } + + async getAssets(authUser: AuthUserDto, id: string): Promise { + await this.findOrFail(authUser, id); + const assets = await this.repository.getAssets(authUser.id, id); + return assets.map(mapAsset); + } + + async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { + await this.findOrFail(authUser, id); + + const results: AssetIdsResponseDto[] = []; + for (const assetId of dto.assetIds) { + const hasAsset = await this.repository.hasAsset(authUser.id, id, assetId); + if (hasAsset) { + results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); + } else { + results.push({ assetId, success: true }); + } + } + + await this.repository.addAssets( + authUser.id, + id, + results.filter((result) => result.success).map((result) => result.assetId), + ); + + return results; + } + + async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { + await this.findOrFail(authUser, id); + + const results: AssetIdsResponseDto[] = []; + for (const assetId of dto.assetIds) { + const hasAsset = await this.repository.hasAsset(authUser.id, id, assetId); + if (!hasAsset) { + results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); + } else { + results.push({ assetId, success: true }); + } + } + + await this.repository.removeAssets( + authUser.id, + id, + results.filter((result) => result.success).map((result) => result.assetId), + ); + + return results; + } + + private async findOrFail(authUser: AuthUserDto, id: string) { + const tag = await this.repository.getById(authUser.id, id); + if (!tag) { + throw new BadRequestException('Tag not found'); + } + return tag; + } +} diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index 3d00e7f24..50cf697cb 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -2,17 +2,19 @@ import { AlbumEntity, APIKeyEntity, AssetEntity, + AssetFaceEntity, AssetType, - PersonEntity, + ExifEntity, PartnerEntity, + PersonEntity, SharedLinkEntity, SharedLinkType, SystemConfig, + TagEntity, + TagType, TranscodePreset, UserEntity, UserTokenEntity, - AssetFaceEntity, - ExifEntity, } from '@app/infra/entities'; import { AlbumResponseDto, @@ -23,6 +25,7 @@ import { mapUser, SearchResult, SharedLinkResponseDto, + TagResponseDto, VideoFormat, VideoInfo, VideoStreamInfo, @@ -988,3 +991,24 @@ export const faceStub = { embedding: [1, 2, 3, 4], }), }; + +export const tagStub = { + tag1: Object.freeze({ + id: 'tag-1', + name: 'Tag1', + type: TagType.CUSTOM, + userId: userEntityStub.admin.id, + user: userEntityStub.admin, + renameTagId: null, + assets: [], + }), +}; + +export const tagResponseStub = { + tag1: Object.freeze({ + id: 'tag-1', + name: 'Tag1', + type: 'CUSTOM', + userId: 'admin_id', + }), +}; diff --git a/server/libs/domain/test/index.ts b/server/libs/domain/test/index.ts index 546b7ced5..f984d366c 100644 --- a/server/libs/domain/test/index.ts +++ b/server/libs/domain/test/index.ts @@ -15,6 +15,7 @@ export * from './shared-link.repository.mock'; export * from './smart-info.repository.mock'; export * from './storage.repository.mock'; export * from './system-config.repository.mock'; +export * from './tag.repository.mock'; export * from './user-token.repository.mock'; export * from './user.repository.mock'; diff --git a/server/libs/domain/test/tag.repository.mock.ts b/server/libs/domain/test/tag.repository.mock.ts new file mode 100644 index 000000000..d42d97e28 --- /dev/null +++ b/server/libs/domain/test/tag.repository.mock.ts @@ -0,0 +1,16 @@ +import { ITagRepository } from '../src'; + +export const newTagRepositoryMock = (): jest.Mocked => { + return { + getAll: jest.fn(), + getById: jest.fn(), + create: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + hasAsset: jest.fn(), + hasName: jest.fn(), + getAssets: jest.fn(), + addAssets: jest.fn(), + removeAssets: jest.fn(), + }; +}; diff --git a/server/libs/infra/src/entities/index.ts b/server/libs/infra/src/entities/index.ts index f166b5927..6864a3f73 100644 --- a/server/libs/infra/src/entities/index.ts +++ b/server/libs/infra/src/entities/index.ts @@ -7,6 +7,7 @@ import { PersonEntity } from './person.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { SmartInfoEntity } from './smart-info.entity'; import { SystemConfigEntity } from './system-config.entity'; +import { TagEntity } from './tag.entity'; import { UserTokenEntity } from './user-token.entity'; import { UserEntity } from './user.entity'; @@ -34,6 +35,7 @@ export const databaseEntities = [ SharedLinkEntity, SmartInfoEntity, SystemConfigEntity, + TagEntity, UserEntity, UserTokenEntity, ]; diff --git a/server/libs/infra/src/entities/tag.entity.ts b/server/libs/infra/src/entities/tag.entity.ts index cad4d4c97..a364529db 100644 --- a/server/libs/infra/src/entities/tag.entity.ts +++ b/server/libs/infra/src/entities/tag.entity.ts @@ -21,7 +21,7 @@ export class TagEntity { userId!: string; @Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true }) - renameTagId!: string; + renameTagId!: string | null; @ManyToMany(() => AssetEntity, (asset) => asset.tags) assets!: AssetEntity[]; diff --git a/server/libs/infra/src/infra.module.ts b/server/libs/infra/src/infra.module.ts index 34cd5f7c8..70b462db1 100644 --- a/server/libs/infra/src/infra.module.ts +++ b/server/libs/infra/src/infra.module.ts @@ -17,6 +17,7 @@ import { ISmartInfoRepository, IStorageRepository, ISystemConfigRepository, + ITagRepository, IUserRepository, IUserTokenRepository, } from '@app/domain'; @@ -45,6 +46,7 @@ import { SharedLinkRepository, SmartInfoRepository, SystemConfigRepository, + TagRepository, TypesenseRepository, UserRepository, UserTokenRepository, @@ -68,6 +70,7 @@ const providers: Provider[] = [ { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, { provide: IStorageRepository, useClass: FilesystemProvider }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, + { provide: ITagRepository, useClass: TagRepository }, { provide: IUserRepository, useClass: UserRepository }, { provide: IUserTokenRepository, useClass: UserTokenRepository }, ]; diff --git a/server/libs/infra/src/repositories/index.ts b/server/libs/infra/src/repositories/index.ts index 75347863e..bb49e336e 100644 --- a/server/libs/infra/src/repositories/index.ts +++ b/server/libs/infra/src/repositories/index.ts @@ -14,6 +14,7 @@ export * from './person.repository'; export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './system-config.repository'; +export * from './tag.repository'; export * from './typesense.repository'; export * from './user-token.repository'; export * from './user.repository'; diff --git a/server/libs/infra/src/repositories/tag.repository.ts b/server/libs/infra/src/repositories/tag.repository.ts new file mode 100644 index 000000000..8713f0650 --- /dev/null +++ b/server/libs/infra/src/repositories/tag.repository.ts @@ -0,0 +1,123 @@ +import { ITagRepository } from '@app/domain'; +import { AssetEntity, TagEntity } from '@app/infra/entities'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class TagRepository implements ITagRepository { + constructor( + @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectRepository(TagEntity) private repository: Repository, + ) {} + + getById(userId: string, id: string): Promise { + return this.repository.findOne({ + where: { + id, + userId, + }, + relations: { + user: true, + }, + }); + } + + getAll(userId: string): Promise { + return this.repository.find({ where: { userId } }); + } + + create(tag: Partial): Promise { + return this.save(tag); + } + + update(tag: Partial): Promise { + return this.save(tag); + } + + async remove(tag: TagEntity): Promise { + await this.repository.remove(tag); + } + + async getAssets(userId: string, tagId: string): Promise { + return this.assetRepository.find({ + where: { + tags: { + userId, + id: tagId, + }, + }, + relations: { + exifInfo: true, + tags: true, + faces: { + person: true, + }, + }, + order: { + createdAt: 'ASC', + }, + }); + } + + async addAssets(userId: string, id: string, assetIds: string[]): Promise { + for (const assetId of assetIds) { + const asset = await this.assetRepository.findOneOrFail({ + where: { + ownerId: userId, + id: assetId, + }, + relations: { + tags: true, + }, + }); + asset.tags.push({ id } as TagEntity); + await this.assetRepository.save(asset); + } + } + + async removeAssets(userId: string, id: string, assetIds: string[]): Promise { + for (const assetId of assetIds) { + const asset = await this.assetRepository.findOneOrFail({ + where: { + ownerId: userId, + id: assetId, + }, + relations: { + tags: true, + }, + }); + asset.tags = asset.tags.filter((tag) => tag.id !== id); + await this.assetRepository.save(asset); + } + } + + hasAsset(userId: string, tagId: string, assetId: string): Promise { + return this.repository.exist({ + where: { + id: tagId, + userId, + assets: { + id: assetId, + }, + }, + relations: { + assets: true, + }, + }); + } + + hasName(userId: string, name: string): Promise { + return this.repository.exist({ + where: { + name, + userId, + }, + }); + } + + private async save(tag: Partial): Promise { + const { id } = await this.repository.save(tag); + return this.repository.findOneOrFail({ where: { id }, relations: { user: true } }); + } +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index e8afa9343..5fbe6bd5d 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -536,6 +536,53 @@ export interface AssetFileUploadResponseDto { */ 'duplicate': boolean; } +/** + * + * @export + * @interface AssetIdsDto + */ +export interface AssetIdsDto { + /** + * + * @type {Array} + * @memberof AssetIdsDto + */ + 'assetIds': Array; +} +/** + * + * @export + * @interface AssetIdsResponseDto + */ +export interface AssetIdsResponseDto { + /** + * + * @type {string} + * @memberof AssetIdsResponseDto + */ + 'assetId': string; + /** + * + * @type {boolean} + * @memberof AssetIdsResponseDto + */ + 'success': boolean; + /** + * + * @type {string} + * @memberof AssetIdsResponseDto + */ + 'error'?: AssetIdsResponseDtoErrorEnum; +} + +export const AssetIdsResponseDtoErrorEnum = { + Duplicate: 'duplicate', + NoPermission: 'no_permission', + NotFound: 'not_found' +} as const; + +export type AssetIdsResponseDtoErrorEnum = typeof AssetIdsResponseDtoErrorEnum[keyof typeof AssetIdsResponseDtoErrorEnum]; + /** * * @export @@ -2420,18 +2467,18 @@ export interface SystemConfigTemplateStorageOptionDto { * @interface TagResponseDto */ export interface TagResponseDto { - /** - * - * @type {string} - * @memberof TagResponseDto - */ - 'id': string; /** * * @type {TagTypeEnum} * @memberof TagResponseDto */ 'type': TagTypeEnum; + /** + * + * @type {string} + * @memberof TagResponseDto + */ + 'id': string; /** * * @type {string} @@ -2444,12 +2491,6 @@ export interface TagResponseDto { * @memberof TagResponseDto */ 'userId': string; - /** - * - * @type {string} - * @memberof TagResponseDto - */ - 'renameTagId'?: string | null; } @@ -2558,12 +2599,6 @@ export interface UpdateTagDto { * @memberof UpdateTagDto */ 'name'?: string; - /** - * - * @type {string} - * @memberof UpdateTagDto - */ - 'renameTagId'?: string; } /** * @@ -10647,57 +10682,15 @@ export class SystemConfigApi extends BaseAPI { */ export const TagApiAxiosParamCreator = function (configuration?: Configuration) { return { - /** - * - * @param {string} id - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - _delete: async (id: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'id' is not null or undefined - assertParamExists('_delete', 'id', id) - const localVarPath = `/tag/{id}` - .replace(`{${"id"}}`, encodeURIComponent(String(id))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {CreateTagDto} createTagDto * @param {*} [options] Override http request option. * @throws {RequiredError} */ - create: async (createTagDto: CreateTagDto, options: AxiosRequestConfig = {}): Promise => { + createTag: async (createTagDto: CreateTagDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'createTagDto' is not null or undefined - assertParamExists('create', 'createTagDto', createTagDto) + assertParamExists('createTag', 'createTagDto', createTagDto) const localVarPath = `/tag`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -10735,10 +10728,52 @@ export const TagApiAxiosParamCreator = function (configuration?: Configuration) }, /** * + * @param {string} id * @param {*} [options] Override http request option. * @throws {RequiredError} */ - findAll: async (options: AxiosRequestConfig = {}): Promise => { + deleteTag: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteTag', 'id', id) + const localVarPath = `/tag/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllTags: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/tag`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -10777,9 +10812,51 @@ export const TagApiAxiosParamCreator = function (configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - findOne: async (id: string, options: AxiosRequestConfig = {}): Promise => { + getTagAssets: async (id: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined - assertParamExists('findOne', 'id', id) + assertParamExists('getTagAssets', 'id', id) + const localVarPath = `/tag/{id}/assets` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTagById: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getTagById', 'id', id) const localVarPath = `/tag/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. @@ -10813,6 +10890,102 @@ export const TagApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + tagAssets: async (id: string, assetIdsDto: AssetIdsDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('tagAssets', 'id', id) + // verify required parameter 'assetIdsDto' is not null or undefined + assertParamExists('tagAssets', 'assetIdsDto', assetIdsDto) + const localVarPath = `/tag/{id}/assets` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + untagAssets: async (id: string, assetIdsDto: AssetIdsDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('untagAssets', 'id', id) + // verify required parameter 'assetIdsDto' is not null or undefined + assertParamExists('untagAssets', 'assetIdsDto', assetIdsDto) + const localVarPath = `/tag/{id}/assets` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetIdsDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} id @@ -10820,11 +10993,11 @@ export const TagApiAxiosParamCreator = function (configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - update: async (id: string, updateTagDto: UpdateTagDto, options: AxiosRequestConfig = {}): Promise => { + updateTag: async (id: string, updateTagDto: UpdateTagDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined - assertParamExists('update', 'id', id) + assertParamExists('updateTag', 'id', id) // verify required parameter 'updateTagDto' is not null or undefined - assertParamExists('update', 'updateTagDto', updateTagDto) + assertParamExists('updateTag', 'updateTagDto', updateTagDto) const localVarPath = `/tag/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. @@ -10871,33 +11044,14 @@ export const TagApiAxiosParamCreator = function (configuration?: Configuration) export const TagApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = TagApiAxiosParamCreator(configuration) return { - /** - * - * @param {string} id - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async _delete(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator._delete(id, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {CreateTagDto} createTagDto * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async create(createTagDto: CreateTagDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.create(createTagDto, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async findAll(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.findAll(options); + async createTag(createTagDto: CreateTagDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createTag(createTagDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -10906,8 +11060,59 @@ export const TagApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async findOne(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.findOne(id, options); + async deleteTag(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTag(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllTags(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllTags(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTagAssets(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTagAssets(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTagById(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTagById(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async tagAssets(id: string, assetIdsDto: AssetIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.tagAssets(id, assetIdsDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async untagAssets(id: string, assetIdsDto: AssetIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.untagAssets(id, assetIdsDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -10917,8 +11122,8 @@ export const TagApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async update(id: string, updateTagDto: UpdateTagDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.update(id, updateTagDto, options); + async updateTag(id: string, updateTagDto: UpdateTagDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateTag(id, updateTagDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -10931,31 +11136,14 @@ export const TagApiFp = function(configuration?: Configuration) { export const TagApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = TagApiFp(configuration) return { - /** - * - * @param {string} id - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - _delete(id: string, options?: any): AxiosPromise { - return localVarFp._delete(id, options).then((request) => request(axios, basePath)); - }, /** * * @param {CreateTagDto} createTagDto * @param {*} [options] Override http request option. * @throws {RequiredError} */ - create(createTagDto: CreateTagDto, options?: any): AxiosPromise { - return localVarFp.create(createTagDto, options).then((request) => request(axios, basePath)); - }, - /** - * - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - findAll(options?: any): AxiosPromise> { - return localVarFp.findAll(options).then((request) => request(axios, basePath)); + createTag(createTagDto: CreateTagDto, options?: any): AxiosPromise { + return localVarFp.createTag(createTagDto, options).then((request) => request(axios, basePath)); }, /** * @@ -10963,8 +11151,54 @@ export const TagApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - findOne(id: string, options?: any): AxiosPromise { - return localVarFp.findOne(id, options).then((request) => request(axios, basePath)); + deleteTag(id: string, options?: any): AxiosPromise { + return localVarFp.deleteTag(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllTags(options?: any): AxiosPromise> { + return localVarFp.getAllTags(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTagAssets(id: string, options?: any): AxiosPromise> { + return localVarFp.getTagAssets(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTagById(id: string, options?: any): AxiosPromise { + return localVarFp.getTagById(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + tagAssets(id: string, assetIdsDto: AssetIdsDto, options?: any): AxiosPromise> { + return localVarFp.tagAssets(id, assetIdsDto, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} id + * @param {AssetIdsDto} assetIdsDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + untagAssets(id: string, assetIdsDto: AssetIdsDto, options?: any): AxiosPromise> { + return localVarFp.untagAssets(id, assetIdsDto, options).then((request) => request(axios, basePath)); }, /** * @@ -10973,71 +11207,127 @@ export const TagApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - update(id: string, updateTagDto: UpdateTagDto, options?: any): AxiosPromise { - return localVarFp.update(id, updateTagDto, options).then((request) => request(axios, basePath)); + updateTag(id: string, updateTagDto: UpdateTagDto, options?: any): AxiosPromise { + return localVarFp.updateTag(id, updateTagDto, options).then((request) => request(axios, basePath)); }, }; }; /** - * Request parameters for _delete operation in TagApi. + * Request parameters for createTag operation in TagApi. * @export - * @interface TagApiDeleteRequest + * @interface TagApiCreateTagRequest */ -export interface TagApiDeleteRequest { - /** - * - * @type {string} - * @memberof TagApiDelete - */ - readonly id: string -} - -/** - * Request parameters for create operation in TagApi. - * @export - * @interface TagApiCreateRequest - */ -export interface TagApiCreateRequest { +export interface TagApiCreateTagRequest { /** * * @type {CreateTagDto} - * @memberof TagApiCreate + * @memberof TagApiCreateTag */ readonly createTagDto: CreateTagDto } /** - * Request parameters for findOne operation in TagApi. + * Request parameters for deleteTag operation in TagApi. * @export - * @interface TagApiFindOneRequest + * @interface TagApiDeleteTagRequest */ -export interface TagApiFindOneRequest { +export interface TagApiDeleteTagRequest { /** * * @type {string} - * @memberof TagApiFindOne + * @memberof TagApiDeleteTag */ readonly id: string } /** - * Request parameters for update operation in TagApi. + * Request parameters for getTagAssets operation in TagApi. * @export - * @interface TagApiUpdateRequest + * @interface TagApiGetTagAssetsRequest */ -export interface TagApiUpdateRequest { +export interface TagApiGetTagAssetsRequest { /** * * @type {string} - * @memberof TagApiUpdate + * @memberof TagApiGetTagAssets + */ + readonly id: string +} + +/** + * Request parameters for getTagById operation in TagApi. + * @export + * @interface TagApiGetTagByIdRequest + */ +export interface TagApiGetTagByIdRequest { + /** + * + * @type {string} + * @memberof TagApiGetTagById + */ + readonly id: string +} + +/** + * Request parameters for tagAssets operation in TagApi. + * @export + * @interface TagApiTagAssetsRequest + */ +export interface TagApiTagAssetsRequest { + /** + * + * @type {string} + * @memberof TagApiTagAssets + */ + readonly id: string + + /** + * + * @type {AssetIdsDto} + * @memberof TagApiTagAssets + */ + readonly assetIdsDto: AssetIdsDto +} + +/** + * Request parameters for untagAssets operation in TagApi. + * @export + * @interface TagApiUntagAssetsRequest + */ +export interface TagApiUntagAssetsRequest { + /** + * + * @type {string} + * @memberof TagApiUntagAssets + */ + readonly id: string + + /** + * + * @type {AssetIdsDto} + * @memberof TagApiUntagAssets + */ + readonly assetIdsDto: AssetIdsDto +} + +/** + * Request parameters for updateTag operation in TagApi. + * @export + * @interface TagApiUpdateTagRequest + */ +export interface TagApiUpdateTagRequest { + /** + * + * @type {string} + * @memberof TagApiUpdateTag */ readonly id: string /** * * @type {UpdateTagDto} - * @memberof TagApiUpdate + * @memberof TagApiUpdateTag */ readonly updateTagDto: UpdateTagDto } @@ -11051,24 +11341,24 @@ export interface TagApiUpdateRequest { export class TagApi extends BaseAPI { /** * - * @param {TagApiDeleteRequest} requestParameters Request parameters. + * @param {TagApiCreateTagRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TagApi */ - public _delete(requestParameters: TagApiDeleteRequest, options?: AxiosRequestConfig) { - return TagApiFp(this.configuration)._delete(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + public createTag(requestParameters: TagApiCreateTagRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).createTag(requestParameters.createTagDto, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {TagApiCreateRequest} requestParameters Request parameters. + * @param {TagApiDeleteTagRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TagApi */ - public create(requestParameters: TagApiCreateRequest, options?: AxiosRequestConfig) { - return TagApiFp(this.configuration).create(requestParameters.createTagDto, options).then((request) => request(this.axios, this.basePath)); + public deleteTag(requestParameters: TagApiDeleteTagRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).deleteTag(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** @@ -11077,30 +11367,63 @@ export class TagApi extends BaseAPI { * @throws {RequiredError} * @memberof TagApi */ - public findAll(options?: AxiosRequestConfig) { - return TagApiFp(this.configuration).findAll(options).then((request) => request(this.axios, this.basePath)); + public getAllTags(options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).getAllTags(options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {TagApiFindOneRequest} requestParameters Request parameters. + * @param {TagApiGetTagAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TagApi */ - public findOne(requestParameters: TagApiFindOneRequest, options?: AxiosRequestConfig) { - return TagApiFp(this.configuration).findOne(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + public getTagAssets(requestParameters: TagApiGetTagAssetsRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).getTagAssets(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } /** * - * @param {TagApiUpdateRequest} requestParameters Request parameters. + * @param {TagApiGetTagByIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof TagApi */ - public update(requestParameters: TagApiUpdateRequest, options?: AxiosRequestConfig) { - return TagApiFp(this.configuration).update(requestParameters.id, requestParameters.updateTagDto, options).then((request) => request(this.axios, this.basePath)); + public getTagById(requestParameters: TagApiGetTagByIdRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).getTagById(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {TagApiTagAssetsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public tagAssets(requestParameters: TagApiTagAssetsRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).tagAssets(requestParameters.id, requestParameters.assetIdsDto, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {TagApiUntagAssetsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public untagAssets(requestParameters: TagApiUntagAssetsRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).untagAssets(requestParameters.id, requestParameters.assetIdsDto, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {TagApiUpdateTagRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TagApi + */ + public updateTag(requestParameters: TagApiUpdateTagRequest, options?: AxiosRequestConfig) { + return TagApiFp(this.configuration).updateTag(requestParameters.id, requestParameters.updateTagDto, options).then((request) => request(this.axios, this.basePath)); } }