diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 948616d6c..5718db449 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -593,6 +593,7 @@ export class AdminService { assetClass: assetClass as AssetClass, assetSubClass: assetSubClass as AssetSubClass, countries: countries as Prisma.JsonArray, + holdings: holdings as Prisma.JsonArray, name: name as string, sectors: sectors as Prisma.JsonArray, url: url as string diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 4857c7e14..0a27faa64 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -38,6 +38,7 @@ import { AuthModule } from './auth/auth.module'; import { CacheModule } from './cache/cache.module'; import { AiModule } from './endpoints/ai/ai.module'; import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; +import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module'; import { AssetsModule } from './endpoints/assets/assets.module'; import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; @@ -69,6 +70,7 @@ import { UserModule } from './user/user.module'; ActivitiesModule, AiModule, ApiKeysModule, + AssetProfilesModule, AssetModule, AssetsModule, AuthDeviceModule, diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts new file mode 100644 index 000000000..2bae2fdb6 --- /dev/null +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts @@ -0,0 +1,41 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; +import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; +import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces'; +import { permissions } from '@ghostfolio/common/permissions'; + +import { + Body, + Controller, + Param, + Patch, + UseGuards, + UseInterceptors +} from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { DataSource } from '@prisma/client'; + +import { AssetProfilesService } from './asset-profiles.service'; + +@Controller('asset-profiles') +export class AssetProfilesController { + public constructor( + private readonly assetProfilesService: AssetProfilesService + ) {} + + @HasPermission(permissions.accessAdminControl) + @Patch(':dataSource/:symbol') + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + public async updateAssetProfileData( + @Body() assetProfileData: UpdateAssetProfileDataDto, + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + return this.assetProfilesService.updateAssetProfileData( + { dataSource, symbol }, + assetProfileData + ); + } +} diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts new file mode 100644 index 000000000..4cf34b8a0 --- /dev/null +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts @@ -0,0 +1,14 @@ +import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; + +import { Module } from '@nestjs/common'; + +import { AssetProfilesController } from './asset-profiles.controller'; +import { AssetProfilesService } from './asset-profiles.service'; + +@Module({ + controllers: [AssetProfilesController], + imports: [SymbolProfileModule, TransformDataSourceInRequestModule], + providers: [AssetProfilesService] +}) +export class AssetProfilesModule {} diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts new file mode 100644 index 000000000..578b6ca6e --- /dev/null +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts @@ -0,0 +1,95 @@ +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; +import { + AssetProfileIdentifier, + EnhancedSymbolProfile +} from '@ghostfolio/common/interfaces'; + +import { Injectable, NotFoundException } from '@nestjs/common'; +import { DataSource, Prisma } from '@prisma/client'; + +@Injectable() +export class AssetProfilesService { + public constructor( + private readonly symbolProfileService: SymbolProfileService + ) {} + + public async updateAssetProfileData( + { dataSource, symbol }: AssetProfileIdentifier, + assetProfileData: UpdateAssetProfileDataDto + ): Promise { + const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ + { + dataSource, + symbol + } + ]); + + if (!assetProfile) { + throw new NotFoundException( + `Asset profile with data source ${dataSource} and symbol ${symbol} not found` + ); + } + + const data = this.getAssetProfileDataUpdate(assetProfileData); + + if (Object.keys(data).length === 0) { + return assetProfile; + } + + await this.symbolProfileService.updateSymbolProfile( + { + dataSource, + symbol + }, + dataSource === DataSource.MANUAL + ? data + : { + SymbolProfileOverrides: { + upsert: { + create: data, + update: data + } + } + } + ); + + const [updatedAssetProfile] = + await this.symbolProfileService.getSymbolProfiles([ + { + dataSource, + symbol + } + ]); + + return updatedAssetProfile; + } + + private getAssetProfileDataUpdate({ + countries, + holdings, + sectors + }: UpdateAssetProfileDataDto): Pick< + Prisma.SymbolProfileUpdateInput, + 'countries' | 'holdings' | 'sectors' + > { + const data: Pick< + Prisma.SymbolProfileUpdateInput, + 'countries' | 'holdings' | 'sectors' + > = {}; + + if (countries !== undefined) { + data.countries = countries as Prisma.JsonArray; + } + + if (holdings !== undefined) { + data.holdings = holdings as Prisma.JsonArray; + } + + if (sectors !== undefined) { + data.sectors = sectors as Prisma.JsonArray; + } + + return data; + } +} diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index 3631d6eae..cf0ce6f57 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -13,6 +13,7 @@ import { DeleteOwnUserDto } from './delete-own-user.dto'; import { TransferBalanceDto } from './transfer-balance.dto'; import { UpdateAccessDto } from './update-access.dto'; import { UpdateAccountDto } from './update-account.dto'; +import { UpdateAssetProfileDataDto } from './update-asset-profile-data.dto'; import { UpdateAssetProfileDto } from './update-asset-profile.dto'; import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto'; import { UpdateMarketDataDto } from './update-market-data.dto'; @@ -39,6 +40,7 @@ export { TransferBalanceDto, UpdateAccessDto, UpdateAccountDto, + UpdateAssetProfileDataDto, UpdateAssetProfileDto, UpdateBulkMarketDataDto, UpdateMarketDataDto, diff --git a/libs/common/src/lib/dtos/update-asset-profile-data.dto.ts b/libs/common/src/lib/dtos/update-asset-profile-data.dto.ts new file mode 100644 index 000000000..a2f600fcd --- /dev/null +++ b/libs/common/src/lib/dtos/update-asset-profile-data.dto.ts @@ -0,0 +1,16 @@ +import { Prisma } from '@prisma/client'; +import { IsArray, IsOptional } from 'class-validator'; + +export class UpdateAssetProfileDataDto { + @IsArray() + @IsOptional() + countries?: Prisma.InputJsonArray; + + @IsArray() + @IsOptional() + holdings?: Prisma.InputJsonArray; + + @IsArray() + @IsOptional() + sectors?: Prisma.InputJsonArray; +}