Feature/create watchlist API endpoints (#4570)
* Create watchlist API endpoints * Update changelog
This commit is contained in:
parent
b77afed38c
commit
416fa44cf0
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the endpoints (`DELETE`, `GET` and `POST`) for the watchlist
|
||||||
|
|
||||||
## 2.154.0 - 2025-04-21
|
## 2.154.0 - 2025-04-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -38,6 +38,7 @@ import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfol
|
|||||||
import { MarketDataModule } from './endpoints/market-data/market-data.module';
|
import { MarketDataModule } from './endpoints/market-data/market-data.module';
|
||||||
import { PublicModule } from './endpoints/public/public.module';
|
import { PublicModule } from './endpoints/public/public.module';
|
||||||
import { TagsModule } from './endpoints/tags/tags.module';
|
import { TagsModule } from './endpoints/tags/tags.module';
|
||||||
|
import { WatchlistModule } from './endpoints/watchlist/watchlist.module';
|
||||||
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
||||||
import { ExportModule } from './export/export.module';
|
import { ExportModule } from './export/export.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
@ -128,7 +129,8 @@ import { UserModule } from './user/user.module';
|
|||||||
SymbolModule,
|
SymbolModule,
|
||||||
TagsModule,
|
TagsModule,
|
||||||
TwitterBotModule,
|
TwitterBotModule,
|
||||||
UserModule
|
UserModule,
|
||||||
|
WatchlistModule
|
||||||
],
|
],
|
||||||
providers: [CronService]
|
providers: [CronService]
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
import { IsEnum, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateWatchlistItemDto {
|
||||||
|
@IsEnum(DataSource)
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
symbol: string;
|
||||||
|
}
|
85
apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
Normal file
85
apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
||||||
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors
|
||||||
|
} 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 { CreateWatchlistItemDto } from './create-watchlist-item.dto';
|
||||||
|
import { WatchlistService } from './watchlist.service';
|
||||||
|
|
||||||
|
@Controller('watchlist')
|
||||||
|
export class WatchlistController {
|
||||||
|
public constructor(
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
|
private readonly watchlistService: WatchlistService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HasPermission(permissions.createWatchlistItem)
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
public async createWatchlistItem(@Body() data: CreateWatchlistItemDto) {
|
||||||
|
return this.watchlistService.createWatchlistItem({
|
||||||
|
dataSource: data.dataSource,
|
||||||
|
symbol: data.symbol,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':dataSource/:symbol')
|
||||||
|
@HasPermission(permissions.deleteWatchlistItem)
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
public async deleteWatchlistItem(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
) {
|
||||||
|
const watchlistItem = await this.watchlistService
|
||||||
|
.getWatchlistItems(this.request.user.id)
|
||||||
|
.then((items) => {
|
||||||
|
return items.find((item) => {
|
||||||
|
return item.dataSource === dataSource && item.symbol === symbol;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!watchlistItem) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.watchlistService.deleteWatchlistItem({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@HasPermission(permissions.readWatchlist)
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getWatchlistItems(): Promise<AssetProfileIdentifier[]> {
|
||||||
|
return this.watchlistService.getWatchlistItems(this.request.user.id);
|
||||||
|
}
|
||||||
|
}
|
19
apps/api/src/app/endpoints/watchlist/watchlist.module.ts
Normal file
19
apps/api/src/app/endpoints/watchlist/watchlist.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WatchlistController } from './watchlist.controller';
|
||||||
|
import { WatchlistService } from './watchlist.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [WatchlistController],
|
||||||
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
|
],
|
||||||
|
providers: [WatchlistService]
|
||||||
|
})
|
||||||
|
export class WatchlistModule {}
|
79
apps/api/src/app/endpoints/watchlist/watchlist.service.ts
Normal file
79
apps/api/src/app/endpoints/watchlist/watchlist.service.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WatchlistService {
|
||||||
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async createWatchlistItem({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const symbolProfile = await this.prismaService.symbolProfile.findUnique({
|
||||||
|
where: {
|
||||||
|
dataSource_symbol: { dataSource, symbol }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!symbolProfile) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Asset profile not found for ${symbol} (${dataSource})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.user.update({
|
||||||
|
data: {
|
||||||
|
watchlist: {
|
||||||
|
connect: {
|
||||||
|
dataSource_symbol: { dataSource, symbol }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: { id: userId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteWatchlistItem({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
await this.prismaService.user.update({
|
||||||
|
data: {
|
||||||
|
watchlist: {
|
||||||
|
disconnect: {
|
||||||
|
dataSource_symbol: { dataSource, symbol }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: { id: userId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getWatchlistItems(
|
||||||
|
userId: string
|
||||||
|
): Promise<AssetProfileIdentifier[]> {
|
||||||
|
const user = await this.prismaService.user.findUnique({
|
||||||
|
select: {
|
||||||
|
watchlist: {
|
||||||
|
select: { dataSource: true, symbol: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: { id: userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return user.watchlist ?? [];
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ export const permissions = {
|
|||||||
createPlatform: 'createPlatform',
|
createPlatform: 'createPlatform',
|
||||||
createTag: 'createTag',
|
createTag: 'createTag',
|
||||||
createUserAccount: 'createUserAccount',
|
createUserAccount: 'createUserAccount',
|
||||||
|
createWatchlistItem: 'createWatchlistItem',
|
||||||
deleteAccess: 'deleteAccess',
|
deleteAccess: 'deleteAccess',
|
||||||
deleteAccount: 'deleteAccount',
|
deleteAccount: 'deleteAccount',
|
||||||
deleteAccountBalance: 'deleteAccountBalance',
|
deleteAccountBalance: 'deleteAccountBalance',
|
||||||
@ -26,6 +27,7 @@ export const permissions = {
|
|||||||
deletePlatform: 'deletePlatform',
|
deletePlatform: 'deletePlatform',
|
||||||
deleteTag: 'deleteTag',
|
deleteTag: 'deleteTag',
|
||||||
deleteUser: 'deleteUser',
|
deleteUser: 'deleteUser',
|
||||||
|
deleteWatchlistItem: 'deleteWatchlistItem',
|
||||||
enableDataProviderGhostfolio: 'enableDataProviderGhostfolio',
|
enableDataProviderGhostfolio: 'enableDataProviderGhostfolio',
|
||||||
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
|
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
|
||||||
enableImport: 'enableImport',
|
enableImport: 'enableImport',
|
||||||
@ -41,6 +43,7 @@ export const permissions = {
|
|||||||
readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile',
|
readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile',
|
||||||
readPlatforms: 'readPlatforms',
|
readPlatforms: 'readPlatforms',
|
||||||
readTags: 'readTags',
|
readTags: 'readTags',
|
||||||
|
readWatchlist: 'readWatchlist',
|
||||||
reportDataGlitch: 'reportDataGlitch',
|
reportDataGlitch: 'reportDataGlitch',
|
||||||
toggleReadOnlyMode: 'toggleReadOnlyMode',
|
toggleReadOnlyMode: 'toggleReadOnlyMode',
|
||||||
updateAccount: 'updateAccount',
|
updateAccount: 'updateAccount',
|
||||||
@ -64,7 +67,9 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.createAccess,
|
permissions.createAccess,
|
||||||
permissions.createAccount,
|
permissions.createAccount,
|
||||||
permissions.createAccountBalance,
|
permissions.createAccountBalance,
|
||||||
|
permissions.createWatchlistItem,
|
||||||
permissions.deleteAccountBalance,
|
permissions.deleteAccountBalance,
|
||||||
|
permissions.deleteWatchlistItem,
|
||||||
permissions.createMarketData,
|
permissions.createMarketData,
|
||||||
permissions.createMarketDataOfOwnAssetProfile,
|
permissions.createMarketDataOfOwnAssetProfile,
|
||||||
permissions.createOrder,
|
permissions.createOrder,
|
||||||
@ -84,6 +89,7 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.readMarketDataOfOwnAssetProfile,
|
permissions.readMarketDataOfOwnAssetProfile,
|
||||||
permissions.readPlatforms,
|
permissions.readPlatforms,
|
||||||
permissions.readTags,
|
permissions.readTags,
|
||||||
|
permissions.readWatchlist,
|
||||||
permissions.updateAccount,
|
permissions.updateAccount,
|
||||||
permissions.updateAuthDevice,
|
permissions.updateAuthDevice,
|
||||||
permissions.updateMarketData,
|
permissions.updateMarketData,
|
||||||
@ -100,7 +106,8 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.accessAssistant,
|
permissions.accessAssistant,
|
||||||
permissions.accessHoldingsChart,
|
permissions.accessHoldingsChart,
|
||||||
permissions.createUserAccount,
|
permissions.createUserAccount,
|
||||||
permissions.readAiPrompt
|
permissions.readAiPrompt,
|
||||||
|
permissions.readWatchlist
|
||||||
];
|
];
|
||||||
|
|
||||||
case 'USER':
|
case 'USER':
|
||||||
@ -113,14 +120,17 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.createMarketDataOfOwnAssetProfile,
|
permissions.createMarketDataOfOwnAssetProfile,
|
||||||
permissions.createOrder,
|
permissions.createOrder,
|
||||||
permissions.createOwnTag,
|
permissions.createOwnTag,
|
||||||
|
permissions.createWatchlistItem,
|
||||||
permissions.deleteAccess,
|
permissions.deleteAccess,
|
||||||
permissions.deleteAccount,
|
permissions.deleteAccount,
|
||||||
permissions.deleteAccountBalance,
|
permissions.deleteAccountBalance,
|
||||||
permissions.deleteAuthDevice,
|
permissions.deleteAuthDevice,
|
||||||
permissions.deleteOrder,
|
permissions.deleteOrder,
|
||||||
permissions.deleteOwnUser,
|
permissions.deleteOwnUser,
|
||||||
|
permissions.deleteWatchlistItem,
|
||||||
permissions.readAiPrompt,
|
permissions.readAiPrompt,
|
||||||
permissions.readMarketDataOfOwnAssetProfile,
|
permissions.readMarketDataOfOwnAssetProfile,
|
||||||
|
permissions.readWatchlist,
|
||||||
permissions.updateAccount,
|
permissions.updateAccount,
|
||||||
permissions.updateAuthDevice,
|
permissions.updateAuthDevice,
|
||||||
permissions.updateMarketDataOfOwnAssetProfile,
|
permissions.updateMarketDataOfOwnAssetProfile,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user