diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 956e916dc..9ea9457a0 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -390,6 +390,16 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRuleDto" + } + } + }, + "required": true + }, "responses": { "201": { "description": "" @@ -423,6 +433,14 @@ "format": "uuid", "type": "string" } + }, + { + "name": "ruleId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } } ], "responses": { @@ -5436,6 +5454,21 @@ ], "type": "object" }, + "CreateRuleDto": { + "properties": { + "key": { + "$ref": "#/components/schemas/RuleKey" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, "CreateTagDto": { "properties": { "name": { @@ -6169,6 +6202,14 @@ ], "type": "object" }, + "RuleKey": { + "enum": [ + "personId", + "exifInfo.city", + "asset.fileCreatedAt" + ], + "type": "string" + }, "SearchAlbumResponseDto": { "properties": { "count": { diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index f98cdfb1f..39b54acff 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -1,4 +1,4 @@ -import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities'; +import { AlbumEntity, AssetEntity, RuleEntity, UserEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, IAccessRepository, Permission } from '../access'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto, IAssetRepository, mapAsset } from '../asset'; @@ -7,7 +7,8 @@ import { IJobRepository, JobName } from '../job'; import { IUserRepository } from '../user'; import { AlbumCountResponseDto, AlbumResponseDto, mapAlbum } from './album-response.dto'; import { IAlbumRepository } from './album.repository'; -import { AddUsersDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto'; +import { AddUsersDto, CreateAlbumDto, CreateRuleDto, GetAlbumsDto, UpdateAlbumDto } from './dto'; +import { IRuleRepository } from './rule.repository'; @Injectable() export class AlbumService { @@ -18,6 +19,7 @@ export class AlbumService { @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IUserRepository) private userRepository: IUserRepository, + @Inject(IRuleRepository) private ruleRepository: IRuleRepository, ) { this.access = new AccessCore(accessRepository); } @@ -98,6 +100,7 @@ export class AlbumService { sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [], assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)), albumThumbnailAssetId: dto.assetIds?.[0] || null, + rules: [], }); await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [album.id] } }); @@ -281,4 +284,29 @@ export class AlbumService { } return album; } + + async addRule(authUser: AuthUserDto, id: string, dto: CreateRuleDto) { + await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); + + const album = await this.findOrFail(id); + const user = await this.userRepository.get(authUser.id); + + if (!user) { + throw new BadRequestException('User not found'); + } + + const rule = new RuleEntity(); + rule.key = dto.key; + rule.value = dto.value; + rule.album = album; + rule.albumId = album.id; + rule.user = user; + rule.ownerId = user.id; + + await this.ruleRepository.create(rule); + + return await this.findOrFail(id); + } + + async removeRule(authUser: AuthUserDto, ruleId: string) {} } diff --git a/server/src/domain/album/dto/index.ts b/server/src/domain/album/dto/index.ts index 4234895f6..73bfcd21b 100644 --- a/server/src/domain/album/dto/index.ts +++ b/server/src/domain/album/dto/index.ts @@ -2,3 +2,4 @@ export * from './album-add-users.dto'; export * from './album-create.dto'; export * from './album-update.dto'; export * from './get-albums.dto'; +export * from './rule.dto'; diff --git a/server/src/domain/album/dto/rule.dto.ts b/server/src/domain/album/dto/rule.dto.ts new file mode 100644 index 000000000..d559a5884 --- /dev/null +++ b/server/src/domain/album/dto/rule.dto.ts @@ -0,0 +1,12 @@ +import { RuleKey } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + +export class CreateRuleDto { + @ApiProperty({ enumName: 'RuleKey', enum: RuleKey }) + @IsNotEmpty() + key!: RuleKey; + + @IsNotEmpty() + value!: string; +} diff --git a/server/src/domain/album/index.ts b/server/src/domain/album/index.ts index 5042b0f44..76530b3dd 100644 --- a/server/src/domain/album/index.ts +++ b/server/src/domain/album/index.ts @@ -2,3 +2,4 @@ export * from './album-response.dto'; export * from './album.repository'; export * from './album.service'; export * from './dto'; +export * from './rule.repository'; diff --git a/server/src/domain/album/rule.repository.ts b/server/src/domain/album/rule.repository.ts new file mode 100644 index 000000000..7908f83a5 --- /dev/null +++ b/server/src/domain/album/rule.repository.ts @@ -0,0 +1,8 @@ +import { RuleEntity } from '@app/infra/entities'; + +export const IRuleRepository = 'IRuleRepository'; + +export interface IRuleRepository { + create(rule: RuleEntity): Promise; + delete(rule: RuleEntity): Promise; +} diff --git a/server/src/immich/controllers/album.controller.ts b/server/src/immich/controllers/album.controller.ts index 445df0bd5..f39da91ee 100644 --- a/server/src/immich/controllers/album.controller.ts +++ b/server/src/immich/controllers/album.controller.ts @@ -6,6 +6,7 @@ import { BulkIdResponseDto, BulkIdsDto, CreateAlbumDto as CreateDto, + CreateRuleDto, UpdateAlbumDto as UpdateDto, } from '@app/domain'; import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto'; @@ -88,12 +89,16 @@ export class AlbumController { } @Post(':id/rule') - addRule(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { - throw new Error('Not implemented'); + addRule(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: CreateRuleDto) { + return this.service.addRule(authUser, id, dto); } @Delete(':id/rule/:ruleId') - removeRule(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { + removeRule( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Param('ruleId', new ParseMeUUIDPipe({ version: '4' })) ruleId: string, + ) { throw new Error('Not implemented'); } } diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index 060c64ae3..8307de81c 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -13,6 +13,7 @@ import { immichAppConfig, IPartnerRepository, IPersonRepository, + IRuleRepository, ISearchRepository, ISharedLinkRepository, ISmartInfoRepository, @@ -45,6 +46,7 @@ import { MediaRepository, PartnerRepository, PersonRepository, + RuleRepository, SharedLinkRepository, SmartInfoRepository, SystemConfigRepository, @@ -67,6 +69,7 @@ const providers: Provider[] = [ { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, { provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPersonRepository, useClass: PersonRepository }, + { provide: IRuleRepository, useClass: RuleRepository }, { provide: ISearchRepository, useClass: TypesenseRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index 850b11f26..4aabf6806 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -24,6 +24,7 @@ export class AlbumRepository implements IAlbumRepository { exifInfo: true, }, sharedLinks: true, + rules: true, }, order: { assets: { @@ -41,6 +42,7 @@ export class AlbumRepository implements IAlbumRepository { relations: { owner: true, sharedUsers: true, + rules: true, }, }); } @@ -48,7 +50,7 @@ export class AlbumRepository implements IAlbumRepository { getByAssetId(ownerId: string, assetId: string): Promise { return this.repository.find({ where: { ownerId, assets: { id: assetId } }, - relations: { owner: true, sharedUsers: true }, + relations: { owner: true, sharedUsers: true, rules: true }, order: { createdAt: 'DESC' }, }); } @@ -104,7 +106,7 @@ export class AlbumRepository implements IAlbumRepository { getOwned(ownerId: string): Promise { return this.repository.find({ - relations: { sharedUsers: true, sharedLinks: true, owner: true }, + relations: { sharedUsers: true, sharedLinks: true, owner: true, rules: true }, where: { ownerId }, order: { createdAt: 'DESC' }, }); @@ -115,7 +117,7 @@ export class AlbumRepository implements IAlbumRepository { */ getShared(ownerId: string): Promise { return this.repository.find({ - relations: { sharedUsers: true, sharedLinks: true, owner: true }, + relations: { sharedUsers: true, sharedLinks: true, owner: true, rules: true }, where: [ { sharedUsers: { id: ownerId } }, { sharedLinks: { userId: ownerId } }, @@ -130,7 +132,7 @@ export class AlbumRepository implements IAlbumRepository { */ getNotShared(ownerId: string): Promise { return this.repository.find({ - relations: { sharedUsers: true, sharedLinks: true, owner: true }, + relations: { sharedUsers: true, sharedLinks: true, owner: true, rules: true }, where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } }, order: { createdAt: 'DESC' }, }); @@ -182,6 +184,7 @@ export class AlbumRepository implements IAlbumRepository { owner: true, sharedUsers: true, assets: true, + rules: true, }, }); } diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 5c7261b2d..57884e925 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -12,6 +12,7 @@ export * from './machine-learning.repository'; export * from './media.repository'; export * from './partner.repository'; export * from './person.repository'; +export * from './rule.repository'; export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './system-config.repository'; diff --git a/server/src/infra/repositories/rule.repository.ts b/server/src/infra/repositories/rule.repository.ts new file mode 100644 index 000000000..49d360efd --- /dev/null +++ b/server/src/infra/repositories/rule.repository.ts @@ -0,0 +1,16 @@ +import { IRuleRepository } from '@app/domain'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { RuleEntity } from '../entities'; + +export class RuleRepository implements IRuleRepository { + constructor(@InjectRepository(RuleEntity) private assetRepository: Repository) {} + + create(rule: RuleEntity): Promise { + return this.assetRepository.save(rule); + } + + delete(rule: RuleEntity): Promise { + throw new Error('Method not implemented.'); + } +}