Browse Source

Feature/extend public API with endpoint to update asset profile data (#6981)

* Extend public API with endpoint to update asset profile data

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
pull/7016/head
Sjohn21 18 hours ago
committed by GitHub
parent
commit
9ea2405fec
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 52
      README.md
  3. 16
      apps/api/src/app/admin/admin.service.ts
  4. 2
      apps/api/src/app/app.module.ts
  5. 51
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  6. 13
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  7. 90
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  8. 27
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  9. 2
      libs/common/src/lib/dtos/index.ts
  10. 16
      libs/common/src/lib/dtos/update-asset-profile-data.dto.ts
  11. 4
      libs/common/src/lib/dtos/update-asset-profile.dto.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Extended the _Public API_ with the endpoint to update the asset profile data (`PATCH api/v1/asset-profiles/:dataSource/:symbol`) (experimental)
- Added support for a dedicated _OpenRouter_ model for the `web_fetch` tool in the `FetchService` - Added support for a dedicated _OpenRouter_ model for the `web_fetch` tool in the `FetchService`
### Changed ### Changed

52
README.md

@ -302,6 +302,58 @@ Grant access of type _Public_ in the _Access_ tab of _My Ghostfolio_.
} }
``` ```
### Update Asset Profile Data (experimental)
#### Prerequisites
[Bearer Token](#authorization-bearer-token) for authorization with admin role
#### Request
`PATCH http://localhost:3333/api/v1/asset-profiles/<INSERT_DATA_SOURCE>/<INSERT_SYMBOL>`
#### Body
```
{
"countries": [
{
"code": "US",
"weight": 1
}
],
"sectors": [
{
"name": "Technology",
"weight": 1
}
]
}
```
| Field | Type | Description |
| ----------- | ------------------ | ---------------------------------------------------------------------- |
| `countries` | `array` (optional) | Countries with `code` (`ISO 3166-1 alpha-2`) and `weight` (`0` to `1`) |
| `holdings` | `array` (optional) | Holdings with `name` and `weight` (`0` to `1`) |
| `sectors` | `array` (optional) | Sectors with `name` and `weight` (`0` to `1`) |
#### Response
##### Success
`200 OK`
##### Error
`404 Not Found`
```
{
"error": "Not Found",
"message": "Could not find the asset profile for MSFT (YAHOO)"
}
```
## Community Projects ## Community Projects
Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio

16
apps/api/src/app/admin/admin.service.ts

@ -593,6 +593,7 @@ export class AdminService {
assetClass: assetClass as AssetClass, assetClass: assetClass as AssetClass,
assetSubClass: assetSubClass as AssetSubClass, assetSubClass: assetSubClass as AssetSubClass,
countries: countries as Prisma.JsonArray, countries: countries as Prisma.JsonArray,
holdings: holdings as Prisma.JsonArray,
name: name as string, name: name as string,
sectors: sectors as Prisma.JsonArray, sectors: sectors as Prisma.JsonArray,
url: url as string url: url as string
@ -602,21 +603,14 @@ export class AdminService {
comment, comment,
currency, currency,
dataSource, dataSource,
holdings,
isActive, isActive,
scraperConfiguration, scraperConfiguration,
symbol, symbol,
symbolMapping, symbolMapping,
...(dataSource === 'MANUAL' ...this.symbolProfileService.getAssetProfileUpdateInput(
? { assetClass, assetSubClass, countries, name, sectors, url } { dataSource, symbol },
: { symbolProfileOverrides
SymbolProfileOverrides: { )
upsert: {
create: symbolProfileOverrides,
update: symbolProfileOverrides
}
}
})
}; };
await this.symbolProfileService.updateSymbolProfile( await this.symbolProfileService.updateSymbolProfile(

2
apps/api/src/app/app.module.ts

@ -38,6 +38,7 @@ import { AuthModule } from './auth/auth.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
import { AiModule } from './endpoints/ai/ai.module'; import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.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 { AssetsModule } from './endpoints/assets/assets.module';
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module'; import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
@ -69,6 +70,7 @@ import { UserModule } from './user/user.module';
ActivitiesModule, ActivitiesModule,
AiModule, AiModule,
ApiKeysModule, ApiKeysModule,
AssetProfilesModule,
AssetModule, AssetModule,
AssetsModule, AssetsModule,
AuthDeviceModule, AuthDeviceModule,

51
apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts

@ -0,0 +1,51 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
HttpException,
Inject,
Param,
Patch,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AssetProfilesService } from './asset-profiles.service';
@Controller('asset-profiles')
export class AssetProfilesController {
public constructor(
private readonly assetProfilesService: AssetProfilesService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.accessAdminControl)
@Patch(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateAssetProfileData(
@Body() assetProfileData: UpdateAssetProfileDataDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<EnhancedSymbolProfile> {
if (!this.request.user.settings.settings.isExperimentalFeatures) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return this.assetProfilesService.updateAssetProfileData(
{ dataSource, symbol },
assetProfileData
);
}
}

13
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -0,0 +1,13 @@
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],
providers: [AssetProfilesService]
})
export class AssetProfilesModule {}

90
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts

@ -0,0 +1,90 @@
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 { Prisma } from '@prisma/client';
@Injectable()
export class AssetProfilesService {
public constructor(
private readonly symbolProfileService: SymbolProfileService
) {}
public async updateAssetProfileData(
{ dataSource, symbol }: AssetProfileIdentifier,
assetProfileData: UpdateAssetProfileDataDto
): Promise<EnhancedSymbolProfile> {
const notFoundMessage = `Could not find the asset profile for ${symbol} (${dataSource})`;
const data = this.getAssetProfileDataUpdate(assetProfileData);
if (Object.keys(data).length > 0) {
try {
await this.symbolProfileService.updateSymbolProfile(
{
dataSource,
symbol
},
this.symbolProfileService.getAssetProfileUpdateInput(
{ dataSource, symbol },
data
)
);
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
) {
throw new NotFoundException(notFoundMessage);
}
throw error;
}
}
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
{
dataSource,
symbol
}
]);
if (!assetProfile) {
throw new NotFoundException(notFoundMessage);
}
return assetProfile;
}
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;
}
}

27
apps/api/src/services/symbol-profile/symbol-profile.service.ts

@ -11,7 +11,12 @@ import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, SymbolProfile, SymbolProfileOverrides } from '@prisma/client'; import {
DataSource,
Prisma,
SymbolProfile,
SymbolProfileOverrides
} from '@prisma/client';
import { continents, countries } from 'countries-list'; import { continents, countries } from 'countries-list';
@Injectable() @Injectable()
@ -71,6 +76,26 @@ export class SymbolProfileService {
}); });
} }
public getAssetProfileUpdateInput(
{ dataSource }: AssetProfileIdentifier,
data: Prisma.SymbolProfileUpdateInput
): Prisma.SymbolProfileUpdateInput {
if (dataSource === DataSource.MANUAL) {
return data;
}
return {
SymbolProfileOverrides: {
upsert: {
create:
data as Prisma.SymbolProfileOverridesCreateWithoutSymbolProfileInput,
update:
data as Prisma.SymbolProfileOverridesUpdateWithoutSymbolProfileInput
}
}
};
}
public async getSymbolProfiles( public async getSymbolProfiles(
aAssetProfileIdentifiers: AssetProfileIdentifier[] aAssetProfileIdentifiers: AssetProfileIdentifier[]
): Promise<EnhancedSymbolProfile[]> { ): Promise<EnhancedSymbolProfile[]> {

2
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 { TransferBalanceDto } from './transfer-balance.dto';
import { UpdateAccessDto } from './update-access.dto'; import { UpdateAccessDto } from './update-access.dto';
import { UpdateAccountDto } from './update-account.dto'; import { UpdateAccountDto } from './update-account.dto';
import { UpdateAssetProfileDataDto } from './update-asset-profile-data.dto';
import { UpdateAssetProfileDto } from './update-asset-profile.dto'; import { UpdateAssetProfileDto } from './update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto'; import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto';
import { UpdateMarketDataDto } from './update-market-data.dto'; import { UpdateMarketDataDto } from './update-market-data.dto';
@ -39,6 +40,7 @@ export {
TransferBalanceDto, TransferBalanceDto,
UpdateAccessDto, UpdateAccessDto,
UpdateAccountDto, UpdateAccountDto,
UpdateAssetProfileDataDto,
UpdateAssetProfileDto, UpdateAssetProfileDto,
UpdateBulkMarketDataDto, UpdateBulkMarketDataDto,
UpdateMarketDataDto, UpdateMarketDataDto,

16
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;
}

4
libs/common/src/lib/dtos/update-asset-profile.dto.ts

@ -36,6 +36,10 @@ export class UpdateAssetProfileDto {
@IsOptional() @IsOptional()
dataSource?: DataSource; dataSource?: DataSource;
@IsArray()
@IsOptional()
holdings?: Prisma.InputJsonArray;
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
isActive?: boolean; isActive?: boolean;

Loading…
Cancel
Save