mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
feat(server) Tagging system (#1046)
This commit is contained in:
parent
6e2763b72c
commit
5de8ea162d
74 changed files with 5429 additions and 94 deletions
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
mobile/openapi/**/*.md -diff -merge
|
||||||
|
mobile/openapi/**/*.md linguist-generated=true
|
||||||
|
mobile/openapi/**/*.dart -diff -merge
|
||||||
|
mobile/openapi/**/*.dart linguist-generated=true
|
||||||
|
|
||||||
|
web/src/api/open-api/**/*.md -diff -merge
|
||||||
|
web/src/api/open-api/**/*.md linguist-generated=true
|
||||||
|
|
||||||
|
web/src/api/open-api/**/*.ts -diff -merge
|
||||||
|
web/src/api/open-api/**/*.ts linguist-generated=true
|
||||||
|
|
||||||
|
mobile/openapi/.openapi-generator/FILES -diff -merge
|
||||||
|
mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
3
Makefile
3
Makefile
|
@ -27,3 +27,6 @@ prod-scale:
|
||||||
|
|
||||||
api:
|
api:
|
||||||
cd ./server && npm run api:generate
|
cd ./server && npm run api:generate
|
||||||
|
|
||||||
|
attach-server:
|
||||||
|
docker exec -it docker_immich-server_1 sh
|
9
NOTES.md
9
NOTES.md
|
@ -1,9 +0,0 @@
|
||||||
# TODO
|
|
||||||
|
|
||||||
Server scenario with web
|
|
||||||
|
|
||||||
[ ] 1 user exist without admin right -> make admin on first check
|
|
||||||
|
|
||||||
[ ] 2 users exist without admin right -> ask user to choose which account will be the admin
|
|
||||||
|
|
||||||
[ X ] No users exist -> prompt signup form for Admin
|
|
20
mobile/openapi/.openapi-generator/FILES
generated
20
mobile/openapi/.openapi-generator/FILES
generated
|
@ -14,6 +14,7 @@ doc/AssetApi.md
|
||||||
doc/AssetCountByTimeBucket.md
|
doc/AssetCountByTimeBucket.md
|
||||||
doc/AssetCountByTimeBucketResponseDto.md
|
doc/AssetCountByTimeBucketResponseDto.md
|
||||||
doc/AssetCountByUserIdResponseDto.md
|
doc/AssetCountByUserIdResponseDto.md
|
||||||
|
doc/AssetEntity.md
|
||||||
doc/AssetFileUploadResponseDto.md
|
doc/AssetFileUploadResponseDto.md
|
||||||
doc/AssetResponseDto.md
|
doc/AssetResponseDto.md
|
||||||
doc/AssetTypeEnum.md
|
doc/AssetTypeEnum.md
|
||||||
|
@ -25,6 +26,7 @@ doc/CheckExistingAssetsResponseDto.md
|
||||||
doc/CreateAlbumDto.md
|
doc/CreateAlbumDto.md
|
||||||
doc/CreateDeviceInfoDto.md
|
doc/CreateDeviceInfoDto.md
|
||||||
doc/CreateProfileImageResponseDto.md
|
doc/CreateProfileImageResponseDto.md
|
||||||
|
doc/CreateTagDto.md
|
||||||
doc/CreateUserDto.md
|
doc/CreateUserDto.md
|
||||||
doc/CuratedLocationsResponseDto.md
|
doc/CuratedLocationsResponseDto.md
|
||||||
doc/CuratedObjectsResponseDto.md
|
doc/CuratedObjectsResponseDto.md
|
||||||
|
@ -34,6 +36,7 @@ doc/DeleteAssetStatus.md
|
||||||
doc/DeviceInfoApi.md
|
doc/DeviceInfoApi.md
|
||||||
doc/DeviceInfoResponseDto.md
|
doc/DeviceInfoResponseDto.md
|
||||||
doc/DeviceTypeEnum.md
|
doc/DeviceTypeEnum.md
|
||||||
|
doc/ExifEntity.md
|
||||||
doc/ExifResponseDto.md
|
doc/ExifResponseDto.md
|
||||||
doc/GetAssetByTimeBucketDto.md
|
doc/GetAssetByTimeBucketDto.md
|
||||||
doc/GetAssetCountByTimeBucketDto.md
|
doc/GetAssetCountByTimeBucketDto.md
|
||||||
|
@ -58,20 +61,27 @@ doc/ServerPingResponse.md
|
||||||
doc/ServerStatsResponseDto.md
|
doc/ServerStatsResponseDto.md
|
||||||
doc/ServerVersionReponseDto.md
|
doc/ServerVersionReponseDto.md
|
||||||
doc/SignUpDto.md
|
doc/SignUpDto.md
|
||||||
|
doc/SmartInfoEntity.md
|
||||||
doc/SmartInfoResponseDto.md
|
doc/SmartInfoResponseDto.md
|
||||||
doc/SystemConfigApi.md
|
doc/SystemConfigApi.md
|
||||||
doc/SystemConfigKey.md
|
doc/SystemConfigKey.md
|
||||||
doc/SystemConfigResponseDto.md
|
doc/SystemConfigResponseDto.md
|
||||||
doc/SystemConfigResponseItem.md
|
doc/SystemConfigResponseItem.md
|
||||||
|
doc/TagApi.md
|
||||||
|
doc/TagEntity.md
|
||||||
|
doc/TagResponseDto.md
|
||||||
|
doc/TagTypeEnum.md
|
||||||
doc/ThumbnailFormat.md
|
doc/ThumbnailFormat.md
|
||||||
doc/TimeGroupEnum.md
|
doc/TimeGroupEnum.md
|
||||||
doc/UpdateAlbumDto.md
|
doc/UpdateAlbumDto.md
|
||||||
doc/UpdateAssetDto.md
|
doc/UpdateAssetDto.md
|
||||||
doc/UpdateDeviceInfoDto.md
|
doc/UpdateDeviceInfoDto.md
|
||||||
|
doc/UpdateTagDto.md
|
||||||
doc/UpdateUserDto.md
|
doc/UpdateUserDto.md
|
||||||
doc/UsageByUserDto.md
|
doc/UsageByUserDto.md
|
||||||
doc/UserApi.md
|
doc/UserApi.md
|
||||||
doc/UserCountResponseDto.md
|
doc/UserCountResponseDto.md
|
||||||
|
doc/UserEntity.md
|
||||||
doc/UserResponseDto.md
|
doc/UserResponseDto.md
|
||||||
doc/ValidateAccessTokenResponseDto.md
|
doc/ValidateAccessTokenResponseDto.md
|
||||||
git_push.sh
|
git_push.sh
|
||||||
|
@ -84,6 +94,7 @@ lib/api/job_api.dart
|
||||||
lib/api/o_auth_api.dart
|
lib/api/o_auth_api.dart
|
||||||
lib/api/server_info_api.dart
|
lib/api/server_info_api.dart
|
||||||
lib/api/system_config_api.dart
|
lib/api/system_config_api.dart
|
||||||
|
lib/api/tag_api.dart
|
||||||
lib/api/user_api.dart
|
lib/api/user_api.dart
|
||||||
lib/api_client.dart
|
lib/api_client.dart
|
||||||
lib/api_exception.dart
|
lib/api_exception.dart
|
||||||
|
@ -103,6 +114,7 @@ lib/model/all_job_status_response_dto.dart
|
||||||
lib/model/asset_count_by_time_bucket.dart
|
lib/model/asset_count_by_time_bucket.dart
|
||||||
lib/model/asset_count_by_time_bucket_response_dto.dart
|
lib/model/asset_count_by_time_bucket_response_dto.dart
|
||||||
lib/model/asset_count_by_user_id_response_dto.dart
|
lib/model/asset_count_by_user_id_response_dto.dart
|
||||||
|
lib/model/asset_entity.dart
|
||||||
lib/model/asset_file_upload_response_dto.dart
|
lib/model/asset_file_upload_response_dto.dart
|
||||||
lib/model/asset_response_dto.dart
|
lib/model/asset_response_dto.dart
|
||||||
lib/model/asset_type_enum.dart
|
lib/model/asset_type_enum.dart
|
||||||
|
@ -113,6 +125,7 @@ lib/model/check_existing_assets_response_dto.dart
|
||||||
lib/model/create_album_dto.dart
|
lib/model/create_album_dto.dart
|
||||||
lib/model/create_device_info_dto.dart
|
lib/model/create_device_info_dto.dart
|
||||||
lib/model/create_profile_image_response_dto.dart
|
lib/model/create_profile_image_response_dto.dart
|
||||||
|
lib/model/create_tag_dto.dart
|
||||||
lib/model/create_user_dto.dart
|
lib/model/create_user_dto.dart
|
||||||
lib/model/curated_locations_response_dto.dart
|
lib/model/curated_locations_response_dto.dart
|
||||||
lib/model/curated_objects_response_dto.dart
|
lib/model/curated_objects_response_dto.dart
|
||||||
|
@ -121,6 +134,7 @@ lib/model/delete_asset_response_dto.dart
|
||||||
lib/model/delete_asset_status.dart
|
lib/model/delete_asset_status.dart
|
||||||
lib/model/device_info_response_dto.dart
|
lib/model/device_info_response_dto.dart
|
||||||
lib/model/device_type_enum.dart
|
lib/model/device_type_enum.dart
|
||||||
|
lib/model/exif_entity.dart
|
||||||
lib/model/exif_response_dto.dart
|
lib/model/exif_response_dto.dart
|
||||||
lib/model/get_asset_by_time_bucket_dto.dart
|
lib/model/get_asset_by_time_bucket_dto.dart
|
||||||
lib/model/get_asset_count_by_time_bucket_dto.dart
|
lib/model/get_asset_count_by_time_bucket_dto.dart
|
||||||
|
@ -142,18 +156,24 @@ lib/model/server_ping_response.dart
|
||||||
lib/model/server_stats_response_dto.dart
|
lib/model/server_stats_response_dto.dart
|
||||||
lib/model/server_version_reponse_dto.dart
|
lib/model/server_version_reponse_dto.dart
|
||||||
lib/model/sign_up_dto.dart
|
lib/model/sign_up_dto.dart
|
||||||
|
lib/model/smart_info_entity.dart
|
||||||
lib/model/smart_info_response_dto.dart
|
lib/model/smart_info_response_dto.dart
|
||||||
lib/model/system_config_key.dart
|
lib/model/system_config_key.dart
|
||||||
lib/model/system_config_response_dto.dart
|
lib/model/system_config_response_dto.dart
|
||||||
lib/model/system_config_response_item.dart
|
lib/model/system_config_response_item.dart
|
||||||
|
lib/model/tag_entity.dart
|
||||||
|
lib/model/tag_response_dto.dart
|
||||||
|
lib/model/tag_type_enum.dart
|
||||||
lib/model/thumbnail_format.dart
|
lib/model/thumbnail_format.dart
|
||||||
lib/model/time_group_enum.dart
|
lib/model/time_group_enum.dart
|
||||||
lib/model/update_album_dto.dart
|
lib/model/update_album_dto.dart
|
||||||
lib/model/update_asset_dto.dart
|
lib/model/update_asset_dto.dart
|
||||||
lib/model/update_device_info_dto.dart
|
lib/model/update_device_info_dto.dart
|
||||||
|
lib/model/update_tag_dto.dart
|
||||||
lib/model/update_user_dto.dart
|
lib/model/update_user_dto.dart
|
||||||
lib/model/usage_by_user_dto.dart
|
lib/model/usage_by_user_dto.dart
|
||||||
lib/model/user_count_response_dto.dart
|
lib/model/user_count_response_dto.dart
|
||||||
|
lib/model/user_entity.dart
|
||||||
lib/model/user_response_dto.dart
|
lib/model/user_response_dto.dart
|
||||||
lib/model/validate_access_token_response_dto.dart
|
lib/model/validate_access_token_response_dto.dart
|
||||||
pubspec.yaml
|
pubspec.yaml
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetEntity.md
generated
Normal file
BIN
mobile/openapi/doc/AssetEntity.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/AssetResponseDto.md
generated
BIN
mobile/openapi/doc/AssetResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/CreateTagDto.md
generated
Normal file
BIN
mobile/openapi/doc/CreateTagDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ExifEntity.md
generated
Normal file
BIN
mobile/openapi/doc/ExifEntity.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/SmartInfoEntity.md
generated
Normal file
BIN
mobile/openapi/doc/SmartInfoEntity.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/TagApi.md
generated
Normal file
BIN
mobile/openapi/doc/TagApi.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/TagEntity.md
generated
Normal file
BIN
mobile/openapi/doc/TagEntity.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/TagResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/TagResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/TagTypeEnum.md
generated
Normal file
BIN
mobile/openapi/doc/TagTypeEnum.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/UpdateAssetDto.md
generated
BIN
mobile/openapi/doc/UpdateAssetDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UpdateTagDto.md
generated
Normal file
BIN
mobile/openapi/doc/UpdateTagDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/UserEntity.md
generated
Normal file
BIN
mobile/openapi/doc/UserEntity.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/tag_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/tag_api.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/album_response_dto.dart
generated
BIN
mobile/openapi/lib/model/album_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_entity.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_entity.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/create_tag_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/create_tag_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/exif_entity.dart
generated
Normal file
BIN
mobile/openapi/lib/model/exif_entity.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/smart_info_entity.dart
generated
Normal file
BIN
mobile/openapi/lib/model/smart_info_entity.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/tag_entity.dart
generated
Normal file
BIN
mobile/openapi/lib/model/tag_entity.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/tag_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/tag_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/tag_type_enum.dart
generated
Normal file
BIN
mobile/openapi/lib/model/tag_type_enum.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/update_asset_dto.dart
generated
BIN
mobile/openapi/lib/model/update_asset_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/update_tag_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/update_tag_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/user_entity.dart
generated
Normal file
BIN
mobile/openapi/lib/model/user_entity.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_entity_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_entity_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/create_tag_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/create_tag_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/exif_entity_test.dart
generated
Normal file
BIN
mobile/openapi/test/exif_entity_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/smart_info_entity_test.dart
generated
Normal file
BIN
mobile/openapi/test/smart_info_entity_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/tag_api_test.dart
generated
Normal file
BIN
mobile/openapi/test/tag_api_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/tag_entity_test.dart
generated
Normal file
BIN
mobile/openapi/test/tag_entity_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/tag_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/tag_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/tag_type_enum_test.dart
generated
Normal file
BIN
mobile/openapi/test/tag_type_enum_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/update_tag_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/update_tag_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/user_entity_test.dart
generated
Normal file
BIN
mobile/openapi/test/user_entity_test.dart
generated
Normal file
Binary file not shown.
|
@ -1,32 +1,29 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from './album.service';
|
||||||
import { AlbumController } from './album.controller';
|
import { AlbumController } from './album.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
|
||||||
import { UserEntity } from '@app/database/entities/user.entity';
|
|
||||||
import { AlbumEntity } from '../../../../../libs/database/src/entities/album.entity';
|
import { AlbumEntity } from '../../../../../libs/database/src/entities/album.entity';
|
||||||
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
||||||
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
||||||
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
||||||
import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository';
|
|
||||||
import { DownloadModule } from '../../modules/download/download.module';
|
import { DownloadModule } from '../../modules/download/download.module';
|
||||||
|
import { AssetModule } from '../asset/asset.module';
|
||||||
|
import { UserModule } from '../user/user.module';
|
||||||
|
|
||||||
|
const ALBUM_REPOSITORY_PROVIDER = {
|
||||||
|
provide: ALBUM_REPOSITORY,
|
||||||
|
useClass: AlbumRepository,
|
||||||
|
};
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity]),
|
TypeOrmModule.forFeature([AlbumEntity, AssetAlbumEntity, UserAlbumEntity]),
|
||||||
DownloadModule,
|
DownloadModule,
|
||||||
|
UserModule,
|
||||||
|
forwardRef(() => AssetModule),
|
||||||
],
|
],
|
||||||
controllers: [AlbumController],
|
controllers: [AlbumController],
|
||||||
providers: [
|
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
||||||
AlbumService,
|
exports: [ALBUM_REPOSITORY_PROVIDER],
|
||||||
{
|
|
||||||
provide: ALBUM_REPOSITORY,
|
|
||||||
useClass: AlbumRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ASSET_REPOSITORY,
|
|
||||||
useClass: AssetRepository,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AlbumModule {}
|
export class AlbumModule {}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm/repository/Repository';
|
import { Repository } from 'typeorm/repository/Repository';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
|
@ -14,6 +14,7 @@ import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
import { In } from 'typeorm/find-options/operator/In';
|
import { In } from 'typeorm/find-options/operator/In';
|
||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
|
import { ITagRepository, TAG_REPOSITORY } from '../tag/tag.repository';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(
|
create(
|
||||||
|
@ -25,7 +26,7 @@ export interface IAssetRepository {
|
||||||
checksum?: Buffer,
|
checksum?: Buffer,
|
||||||
livePhotoAssetEntity?: AssetEntity,
|
livePhotoAssetEntity?: AssetEntity,
|
||||||
): Promise<AssetEntity>;
|
): Promise<AssetEntity>;
|
||||||
update(asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
||||||
getAllByUserId(userId: string, skip?: number): Promise<AssetEntity[]>;
|
getAllByUserId(userId: string, skip?: number): Promise<AssetEntity[]>;
|
||||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||||
getById(assetId: string): Promise<AssetEntity>;
|
getById(assetId: string): Promise<AssetEntity>;
|
||||||
|
@ -53,6 +54,8 @@ export class AssetRepository implements IAssetRepository {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
|
|
||||||
|
@Inject(TAG_REPOSITORY) private _tagRepository: ITagRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAssetWithNoSmartInfo(): Promise<AssetEntity[]> {
|
async getAssetWithNoSmartInfo(): Promise<AssetEntity[]> {
|
||||||
|
@ -222,7 +225,7 @@ export class AssetRepository implements IAssetRepository {
|
||||||
where: {
|
where: {
|
||||||
id: assetId,
|
id: assetId,
|
||||||
},
|
},
|
||||||
relations: ['exifInfo'],
|
relations: ['exifInfo', 'tags'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,9 +240,9 @@ export class AssetRepository implements IAssetRepository {
|
||||||
.andWhere('asset.resizePath is not NULL')
|
.andWhere('asset.resizePath is not NULL')
|
||||||
.andWhere('asset.isVisible = true')
|
.andWhere('asset.isVisible = true')
|
||||||
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
||||||
|
.leftJoinAndSelect('asset.tags', 'tags')
|
||||||
.skip(skip || 0)
|
.skip(skip || 0)
|
||||||
.orderBy('asset.createdAt', 'DESC');
|
.orderBy('asset.createdAt', 'DESC');
|
||||||
|
|
||||||
return await query.getMany();
|
return await query.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,9 +289,14 @@ export class AssetRepository implements IAssetRepository {
|
||||||
/**
|
/**
|
||||||
* Update asset
|
* Update asset
|
||||||
*/
|
*/
|
||||||
async update(asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
|
async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
|
||||||
asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
|
asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
|
||||||
|
|
||||||
|
if (dto.tagIds) {
|
||||||
|
const tags = await this._tagRepository.getByIds(userId, dto.tagIds);
|
||||||
|
asset.tags = tags;
|
||||||
|
}
|
||||||
|
|
||||||
return await this.assetRepository.save(asset);
|
return await this.assetRepository.save(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,10 +355,10 @@ export class AssetRepository implements IAssetRepository {
|
||||||
|
|
||||||
async countByIdAndUser(assetId: string, userId: string): Promise<number> {
|
async countByIdAndUser(assetId: string, userId: string): Promise<number> {
|
||||||
return await this.assetRepository.count({
|
return await this.assetRepository.count({
|
||||||
where: {
|
where: {
|
||||||
id: assetId,
|
id: assetId,
|
||||||
userId
|
userId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,14 +216,14 @@ export class AssetController {
|
||||||
/**
|
/**
|
||||||
* Update an asset
|
* Update an asset
|
||||||
*/
|
*/
|
||||||
@Put('/assetById/:assetId')
|
@Put('/:assetId')
|
||||||
async updateAssetById(
|
async updateAsset(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@Param('assetId') assetId: string,
|
@Param('assetId') assetId: string,
|
||||||
@Body() dto: UpdateAssetDto,
|
@Body(ValidationPipe) dto: UpdateAssetDto,
|
||||||
): Promise<AssetResponseDto> {
|
): Promise<AssetResponseDto> {
|
||||||
await this.assetService.checkAssetsAccess(authUser, [assetId], true);
|
await this.assetService.checkAssetsAccess(authUser, [assetId], true);
|
||||||
return await this.assetService.updateAssetById(assetId, dto);
|
return await this.assetService.updateAsset(authUser, assetId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('/')
|
@Delete('/')
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { AssetController } from './asset.controller';
|
import { AssetController } from './asset.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
@ -10,18 +10,25 @@ import { CommunicationModule } from '../communication/communication.module';
|
||||||
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
|
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
|
||||||
import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
|
import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
|
||||||
import { DownloadModule } from '../../modules/download/download.module';
|
import { DownloadModule } from '../../modules/download/download.module';
|
||||||
import { ALBUM_REPOSITORY, AlbumRepository } from '../album/album-repository';
|
import { TagModule } from '../tag/tag.module';
|
||||||
import { AlbumEntity } from '@app/database/entities/album.entity';
|
import { AlbumModule } from '../album/album.module';
|
||||||
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
import { UserModule } from '../user/user.module';
|
||||||
import { UserEntity } from '@app/database/entities/user.entity';
|
|
||||||
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
const ASSET_REPOSITORY_PROVIDER = {
|
||||||
|
provide: ASSET_REPOSITORY,
|
||||||
|
useClass: AssetRepository,
|
||||||
|
};
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([AssetEntity]),
|
||||||
CommunicationModule,
|
CommunicationModule,
|
||||||
BackgroundTaskModule,
|
BackgroundTaskModule,
|
||||||
DownloadModule,
|
DownloadModule,
|
||||||
TypeOrmModule.forFeature([AssetEntity, AlbumEntity, UserAlbumEntity, UserEntity, AssetAlbumEntity]),
|
UserModule,
|
||||||
|
AlbumModule,
|
||||||
|
TagModule,
|
||||||
|
forwardRef(() => AlbumModule),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: QueueNameEnum.ASSET_UPLOADED,
|
name: QueueNameEnum.ASSET_UPLOADED,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
|
@ -40,18 +47,7 @@ import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AssetController],
|
controllers: [AssetController],
|
||||||
providers: [
|
providers: [AssetService, BackgroundTaskService, ASSET_REPOSITORY_PROVIDER],
|
||||||
AssetService,
|
exports: [ASSET_REPOSITORY_PROVIDER],
|
||||||
BackgroundTaskService,
|
|
||||||
{
|
|
||||||
provide: ASSET_REPOSITORY,
|
|
||||||
useClass: AssetRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ALBUM_REPOSITORY,
|
|
||||||
useClass: AlbumRepository,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [AssetService],
|
|
||||||
})
|
})
|
||||||
export class AssetModule {}
|
export class AssetModule {}
|
||||||
|
|
|
@ -231,13 +231,13 @@ export class AssetService {
|
||||||
return mapAsset(asset);
|
return mapAsset(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAssetById(assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
const asset = await this._assetRepository.getById(assetId);
|
const asset = await this._assetRepository.getById(assetId);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset not found');
|
throw new BadRequestException('Asset not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAsset = await this._assetRepository.update(asset, dto);
|
const updatedAsset = await this._assetRepository.update(authUser.id, asset, dto);
|
||||||
|
|
||||||
return mapAsset(updatedAsset);
|
return mapAsset(updatedAsset);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
import { IsBoolean } from 'class-validator';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAssetDto {
|
export class UpdateAssetDto {
|
||||||
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isFavorite!: boolean;
|
isFavorite?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsNotEmpty({ each: true })
|
||||||
|
@ApiProperty({
|
||||||
|
isArray: true,
|
||||||
|
type: String,
|
||||||
|
title: 'Array of tag IDs to add to the asset',
|
||||||
|
example: [
|
||||||
|
'bf973405-3f2a-48d2-a687-2ed4167164be',
|
||||||
|
'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
|
||||||
|
'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
tagIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { mapTag, TagResponseDto } from '../../tag/response-dto/tag-response.dto';
|
||||||
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
||||||
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ export class AssetResponseDto {
|
||||||
exifInfo?: ExifResponseDto;
|
exifInfo?: ExifResponseDto;
|
||||||
smartInfo?: SmartInfoResponseDto;
|
smartInfo?: SmartInfoResponseDto;
|
||||||
livePhotoVideoId?: string | null;
|
livePhotoVideoId?: string | null;
|
||||||
|
tags!: TagResponseDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
||||||
|
@ -44,5 +46,6 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
||||||
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
||||||
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
||||||
livePhotoVideoId: entity.livePhotoVideoId,
|
livePhotoVideoId: entity.livePhotoVideoId,
|
||||||
|
tags: entity.tags?.map(mapTag),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,21 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||||
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { jwtConfig } from '../../config/jwt.config';
|
import { jwtConfig } from '../../config/jwt.config';
|
||||||
import { UserEntity } from '@app/database/entities/user.entity';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { QueueNameEnum } from '@app/job';
|
import { QueueNameEnum } from '@app/job';
|
||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
|
||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||||
import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository';
|
import { TagModule } from '../tag/tag.module';
|
||||||
|
import { AssetModule } from '../asset/asset.module';
|
||||||
|
import { UserModule } from '../user/user.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([UserEntity, AssetEntity, ExifEntity]),
|
TypeOrmModule.forFeature([ExifEntity]),
|
||||||
ImmichJwtModule,
|
ImmichJwtModule,
|
||||||
|
TagModule,
|
||||||
|
AssetModule,
|
||||||
|
UserModule,
|
||||||
JwtModule.register(jwtConfig),
|
JwtModule.register(jwtConfig),
|
||||||
BullModule.registerQueue(
|
BullModule.registerQueue(
|
||||||
{
|
{
|
||||||
|
@ -70,13 +73,6 @@ import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository';
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
controllers: [JobController],
|
controllers: [JobController],
|
||||||
providers: [
|
providers: [JobService, ImmichJwtService],
|
||||||
JobService,
|
|
||||||
ImmichJwtService,
|
|
||||||
{
|
|
||||||
provide: ASSET_REPOSITORY,
|
|
||||||
useClass: AssetRepository,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class JobModule {}
|
export class JobModule {}
|
||||||
|
|
14
server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts
Normal file
14
server/apps/immich/src/api-v1/tag/dto/create-tag.dto.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { TagType } from '@app/database/entities/tag.entity';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateTagDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsEnum(TagType)
|
||||||
|
@IsNotEmpty()
|
||||||
|
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
|
||||||
|
type!: TagType;
|
||||||
|
}
|
11
server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts
Normal file
11
server/apps/immich/src/api-v1/tag/dto/update-tag.dto.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateTagDto {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
renameTagId?: string;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TagEntity, TagType } from '@app/database/entities/tag.entity';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TagResponseDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
name!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapTag(entity: TagEntity): TagResponseDto {
|
||||||
|
return {
|
||||||
|
id: entity.id,
|
||||||
|
type: entity.type,
|
||||||
|
name: entity.name,
|
||||||
|
};
|
||||||
|
}
|
44
server/apps/immich/src/api-v1/tag/tag.controller.ts
Normal file
44
server/apps/immich/src/api-v1/tag/tag.controller.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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 { TagEntity } from '@app/database/entities/tag.entity';
|
||||||
|
|
||||||
|
@Authenticated()
|
||||||
|
@ApiTags('Tag')
|
||||||
|
@Controller('tag')
|
||||||
|
export class TagController {
|
||||||
|
constructor(private readonly tagService: TagService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createTagDto: CreateTagDto): Promise<TagEntity> {
|
||||||
|
return this.tagService.create(authUser, createTagDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(@GetAuthUser() authUser: AuthUserDto) {
|
||||||
|
return this.tagService.findAll(authUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string) {
|
||||||
|
return this.tagService.findOne(authUser, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(
|
||||||
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body(ValidationPipe) updateTagDto: UpdateTagDto,
|
||||||
|
) {
|
||||||
|
return this.tagService.update(authUser, id, updateTagDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
delete(@GetAuthUser() authUser: AuthUserDto, @Param('id') id: string) {
|
||||||
|
return this.tagService.remove(authUser, id);
|
||||||
|
}
|
||||||
|
}
|
18
server/apps/immich/src/api-v1/tag/tag.module.ts
Normal file
18
server/apps/immich/src/api-v1/tag/tag.module.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TagService } from './tag.service';
|
||||||
|
import { TagController } from './tag.controller';
|
||||||
|
import { TagEntity } from '@app/database/entities/tag.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TagRepository, TAG_REPOSITORY } from './tag.repository';
|
||||||
|
|
||||||
|
const TAG_REPOSITORY_PROVIDER = {
|
||||||
|
provide: TAG_REPOSITORY,
|
||||||
|
useClass: TagRepository,
|
||||||
|
};
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([TagEntity])],
|
||||||
|
controllers: [TagController],
|
||||||
|
providers: [TagService, TAG_REPOSITORY_PROVIDER],
|
||||||
|
exports: [TAG_REPOSITORY_PROVIDER],
|
||||||
|
})
|
||||||
|
export class TagModule {}
|
61
server/apps/immich/src/api-v1/tag/tag.repository.ts
Normal file
61
server/apps/immich/src/api-v1/tag/tag.repository.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { TagEntity, TagType } from '@app/database/entities/tag.entity';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { In, Repository } from 'typeorm';
|
||||||
|
import { UpdateTagDto } from './dto/update-tag.dto';
|
||||||
|
|
||||||
|
export interface ITagRepository {
|
||||||
|
create(userId: string, tagType: TagType, tagName: string): Promise<TagEntity>;
|
||||||
|
getByIds(userId: string, tagIds: string[]): Promise<TagEntity[]>;
|
||||||
|
getById(tagId: string, userId: string): Promise<TagEntity | null>;
|
||||||
|
getByUserId(userId: string): Promise<TagEntity[]>;
|
||||||
|
update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise<TagEntity | null>;
|
||||||
|
remove(tag: TagEntity): Promise<TagEntity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TAG_REPOSITORY = 'TAG_REPOSITORY';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TagRepository implements ITagRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(TagEntity)
|
||||||
|
private tagRepository: Repository<TagEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(userId: string, tagType: TagType, tagName: string): Promise<TagEntity> {
|
||||||
|
const tag = new TagEntity();
|
||||||
|
tag.name = tagName;
|
||||||
|
tag.type = tagType;
|
||||||
|
tag.userId = userId;
|
||||||
|
|
||||||
|
return this.tagRepository.save(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(tagId: string, userId: string): Promise<TagEntity | null> {
|
||||||
|
return await this.tagRepository.findOne({ where: { id: tagId, userId }, relations: ['user'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByIds(userId: string, tagIds: string[]): Promise<TagEntity[]> {
|
||||||
|
return await this.tagRepository.find({
|
||||||
|
where: { id: In(tagIds), userId },
|
||||||
|
relations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByUserId(userId: string): Promise<TagEntity[]> {
|
||||||
|
return await this.tagRepository.find({ where: { userId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(tag: TagEntity, updateTagDto: UpdateTagDto): Promise<TagEntity> {
|
||||||
|
tag.name = updateTagDto.name ?? tag.name;
|
||||||
|
tag.renameTagId = updateTagDto.renameTagId ?? tag.renameTagId;
|
||||||
|
|
||||||
|
return this.tagRepository.save(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(tag: TagEntity): Promise<TagEntity> {
|
||||||
|
return await this.tagRepository.remove(tag);
|
||||||
|
}
|
||||||
|
}
|
91
server/apps/immich/src/api-v1/tag/tag.service.spec.ts
Normal file
91
server/apps/immich/src/api-v1/tag/tag.service.spec.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { TagEntity, TagType } from '@app/database/entities/tag.entity';
|
||||||
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
|
import { ITagRepository } from './tag.repository';
|
||||||
|
import { TagService } from './tag.service';
|
||||||
|
|
||||||
|
describe('TagService', () => {
|
||||||
|
let sut: TagService;
|
||||||
|
let tagRepositoryMock: jest.Mocked<ITagRepository>;
|
||||||
|
|
||||||
|
const user1AuthUser: AuthUserDto = Object.freeze({
|
||||||
|
id: '1111',
|
||||||
|
email: 'testuser@email.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1: UserEntity = Object.freeze({
|
||||||
|
id: '1111',
|
||||||
|
firstName: 'Alex',
|
||||||
|
lastName: 'Tran',
|
||||||
|
isAdmin: true,
|
||||||
|
email: 'testuser@email.com',
|
||||||
|
profileImagePath: '',
|
||||||
|
shouldChangePassword: true,
|
||||||
|
createdAt: '2022-12-02T19:29:23.603Z',
|
||||||
|
deletedAt: undefined,
|
||||||
|
tags: [],
|
||||||
|
oauthId: 'oauth-id-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
48
server/apps/immich/src/api-v1/tag/tag.service.ts
Normal file
48
server/apps/immich/src/api-v1/tag/tag.service.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { TagEntity } from '@app/database/entities/tag.entity';
|
||||||
|
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, TAG_REPOSITORY } from './tag.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TagService {
|
||||||
|
readonly logger = new Logger(TagService.name);
|
||||||
|
|
||||||
|
constructor(@Inject(TAG_REPOSITORY) private _tagRepository: ITagRepository) {}
|
||||||
|
|
||||||
|
async create(authUser: AuthUserDto, createTagDto: CreateTagDto) {
|
||||||
|
try {
|
||||||
|
return await this._tagRepository.create(authUser.id, createTagDto.type, createTagDto.name);
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error(e, e.stack);
|
||||||
|
throw new BadRequestException(`Failed to create tag: ${e.detail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(authUser: AuthUserDto) {
|
||||||
|
return await this._tagRepository.getByUserId(authUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(authUser: AuthUserDto, id: string): Promise<TagEntity> {
|
||||||
|
const tag = await this._tagRepository.getById(id, authUser.id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
throw new BadRequestException('Tag not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(authUser: AuthUserDto, id: string, updateTagDto: UpdateTagDto) {
|
||||||
|
const tag = await this.findOne(authUser, id);
|
||||||
|
|
||||||
|
return this._tagRepository.update(tag, updateTagDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(authUser: AuthUserDto, id: string) {
|
||||||
|
const tag = await this.findOne(authUser, id);
|
||||||
|
|
||||||
|
return this._tagRepository.remove(tag);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ describe('UserService', () => {
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const immichUser: UserEntity = Object.freeze({
|
const immichUser: UserEntity = Object.freeze({
|
||||||
|
@ -45,6 +46,7 @@ describe('UserService', () => {
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedImmichUser: UserEntity = Object.freeze({
|
const updatedImmichUser: UserEntity = Object.freeze({
|
||||||
|
@ -59,6 +61,7 @@ describe('UserService', () => {
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { DatabaseModule } from '@app/database';
|
||||||
import { JobModule } from './api-v1/job/job.module';
|
import { JobModule } from './api-v1/job/job.module';
|
||||||
import { SystemConfigModule } from './api-v1/system-config/system-config.module';
|
import { SystemConfigModule } from './api-v1/system-config/system-config.module';
|
||||||
import { OAuthModule } from './api-v1/oauth/oauth.module';
|
import { OAuthModule } from './api-v1/oauth/oauth.module';
|
||||||
|
import { TagModule } from './api-v1/tag/tag.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -63,6 +64,8 @@ import { OAuthModule } from './api-v1/oauth/oauth.module';
|
||||||
JobModule,
|
JobModule,
|
||||||
|
|
||||||
SystemConfigModule,
|
SystemConfigModule,
|
||||||
|
|
||||||
|
TagModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
|
@ -55,7 +55,7 @@ async function bootstrap() {
|
||||||
if (process.env.NODE_ENV == 'development') {
|
if (process.env.NODE_ENV == 'development') {
|
||||||
// Generate API Documentation only in development mode
|
// Generate API Documentation only in development mode
|
||||||
const outputPath = path.resolve(process.cwd(), 'immich-openapi-specs.json');
|
const outputPath = path.resolve(process.cwd(), 'immich-openapi-specs.json');
|
||||||
writeFileSync(outputPath, JSON.stringify(apiDocument), { encoding: 'utf8' });
|
writeFileSync(outputPath, JSON.stringify(apiDocument, null, 2), { encoding: 'utf8' });
|
||||||
Logger.log(
|
Logger.log(
|
||||||
`Running Immich Server in DEVELOPMENT environment - version ${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`,
|
`Running Immich Server in DEVELOPMENT environment - version ${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`,
|
||||||
'ImmichServer',
|
'ImmichServer',
|
||||||
|
|
|
@ -56,6 +56,7 @@ describe('ImmichJwtService', () => {
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
createdAt: 'today',
|
createdAt: 'today',
|
||||||
|
tags: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const dto: LoginResponseDto = {
|
const dto: LoginResponseDto = {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,7 @@
|
||||||
import { Column, Entity, Index, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
import { Column, Entity, Index, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
import { ExifEntity } from './exif.entity';
|
import { ExifEntity } from './exif.entity';
|
||||||
import { SmartInfoEntity } from './smart-info.entity';
|
import { SmartInfoEntity } from './smart-info.entity';
|
||||||
|
import { TagEntity } from './tag.entity';
|
||||||
|
|
||||||
@Entity('assets')
|
@Entity('assets')
|
||||||
@Unique('UQ_userid_checksum', ['userId', 'checksum'])
|
@Unique('UQ_userid_checksum', ['userId', 'checksum'])
|
||||||
|
@ -62,6 +63,11 @@ export class AssetEntity {
|
||||||
|
|
||||||
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
|
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
|
||||||
smartInfo?: SmartInfoEntity;
|
smartInfo?: SmartInfoEntity;
|
||||||
|
|
||||||
|
// https://github.com/typeorm/typeorm/blob/master/docs/many-to-many-relations.md
|
||||||
|
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
|
||||||
|
@JoinTable({ name: 'tag_asset' })
|
||||||
|
tags!: TagEntity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AssetType {
|
export enum AssetType {
|
||||||
|
|
45
server/libs/database/src/entities/tag.entity.ts
Normal file
45
server/libs/database/src/entities/tag.entity.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
import { AssetEntity } from './asset.entity';
|
||||||
|
import { UserEntity } from './user.entity';
|
||||||
|
|
||||||
|
@Entity('tags')
|
||||||
|
@Unique('UQ_tag_name_userId', ['name', 'userId'])
|
||||||
|
export class TagEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
type!: TagType;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true })
|
||||||
|
renameTagId!: string;
|
||||||
|
|
||||||
|
@ManyToMany(() => AssetEntity, (asset) => asset.tags)
|
||||||
|
assets!: AssetEntity[];
|
||||||
|
|
||||||
|
@ManyToOne(() => UserEntity, (user) => user.tags)
|
||||||
|
user!: UserEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TagType {
|
||||||
|
/**
|
||||||
|
* Tag that is detected by the ML model for object detection will use this type
|
||||||
|
*/
|
||||||
|
OBJECT = 'OBJECT',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Face that is detected by the ML model for facial detection (TBD/NOT YET IMPLEMENTED) will use this type
|
||||||
|
*/
|
||||||
|
FACE = 'FACE',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag that is created by the user will use this type
|
||||||
|
*/
|
||||||
|
CUSTOM = 'CUSTOM',
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { TagEntity } from './tag.entity';
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
export class UserEntity {
|
export class UserEntity {
|
||||||
|
@ -37,4 +38,7 @@ export class UserEntity {
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => TagEntity, (tag) => tag.user)
|
||||||
|
tags!: TagEntity[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateTagsTable1670257571385 implements MigrationInterface {
|
||||||
|
name = 'CreateTagsTable1670257571385'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL, "name" character varying NOT NULL, "userId" uuid NOT NULL, "renameTagId" uuid, CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId"), CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id")); COMMENT ON COLUMN "tags"."renameTagId" IS 'The new renamed tagId'`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL, CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_f8e8a9e893cb5c54907f1b798e" ON "tag_asset" ("assetsId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" ON "tag_asset" ("tagsId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e99f31ea4cdf3a2c35c7287eb4"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_f8e8a9e893cb5c54907f1b798e"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "tag_asset"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "tags"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
943
web/src/api/open-api/api.ts
generated
943
web/src/api/open-api/api.ts
generated
File diff suppressed because it is too large
Load diff
|
@ -183,7 +183,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleFavorite = async () => {
|
const toggleFavorite = async () => {
|
||||||
const { data } = await api.assetApi.updateAssetById(asset.id, {
|
const { data } = await api.assetApi.updateAsset(asset.id, {
|
||||||
isFavorite: !asset.isFavorite
|
isFavorite: !asset.isFavorite
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,9 @@
|
||||||
style={`width: ${getStorageUsagePercentage()}%`}
|
style={`width: ${getStorageUsagePercentage()}%`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs">{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used</p>
|
<p class="text-xs">
|
||||||
|
{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|
|
@ -115,9 +115,7 @@
|
||||||
<input
|
<input
|
||||||
disabled
|
disabled
|
||||||
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
|
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
|
||||||
value={`[${getBytesWithUnit(uploadAsset.file.size)}] ${
|
value={`[${getBytesWithUnit(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
|
||||||
uploadAsset.file.name
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
|
||||||
|
|
Loading…
Reference in a new issue