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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
@ -38,6 +38,7 @@ import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfol
|
||||
import { MarketDataModule } from './endpoints/market-data/market-data.module';
|
||||
import { PublicModule } from './endpoints/public/public.module';
|
||||
import { TagsModule } from './endpoints/tags/tags.module';
|
||||
import { WatchlistModule } from './endpoints/watchlist/watchlist.module';
|
||||
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
||||
import { ExportModule } from './export/export.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
@ -128,7 +129,8 @@ import { UserModule } from './user/user.module';
|
||||
SymbolModule,
|
||||
TagsModule,
|
||||
TwitterBotModule,
|
||||
UserModule
|
||||
UserModule,
|
||||
WatchlistModule
|
||||
],
|
||||
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',
|
||||
createTag: 'createTag',
|
||||
createUserAccount: 'createUserAccount',
|
||||
createWatchlistItem: 'createWatchlistItem',
|
||||
deleteAccess: 'deleteAccess',
|
||||
deleteAccount: 'deleteAccount',
|
||||
deleteAccountBalance: 'deleteAccountBalance',
|
||||
@ -26,6 +27,7 @@ export const permissions = {
|
||||
deletePlatform: 'deletePlatform',
|
||||
deleteTag: 'deleteTag',
|
||||
deleteUser: 'deleteUser',
|
||||
deleteWatchlistItem: 'deleteWatchlistItem',
|
||||
enableDataProviderGhostfolio: 'enableDataProviderGhostfolio',
|
||||
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
|
||||
enableImport: 'enableImport',
|
||||
@ -41,6 +43,7 @@ export const permissions = {
|
||||
readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile',
|
||||
readPlatforms: 'readPlatforms',
|
||||
readTags: 'readTags',
|
||||
readWatchlist: 'readWatchlist',
|
||||
reportDataGlitch: 'reportDataGlitch',
|
||||
toggleReadOnlyMode: 'toggleReadOnlyMode',
|
||||
updateAccount: 'updateAccount',
|
||||
@ -64,7 +67,9 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.createAccess,
|
||||
permissions.createAccount,
|
||||
permissions.createAccountBalance,
|
||||
permissions.createWatchlistItem,
|
||||
permissions.deleteAccountBalance,
|
||||
permissions.deleteWatchlistItem,
|
||||
permissions.createMarketData,
|
||||
permissions.createMarketDataOfOwnAssetProfile,
|
||||
permissions.createOrder,
|
||||
@ -84,6 +89,7 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.readMarketDataOfOwnAssetProfile,
|
||||
permissions.readPlatforms,
|
||||
permissions.readTags,
|
||||
permissions.readWatchlist,
|
||||
permissions.updateAccount,
|
||||
permissions.updateAuthDevice,
|
||||
permissions.updateMarketData,
|
||||
@ -100,7 +106,8 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.accessAssistant,
|
||||
permissions.accessHoldingsChart,
|
||||
permissions.createUserAccount,
|
||||
permissions.readAiPrompt
|
||||
permissions.readAiPrompt,
|
||||
permissions.readWatchlist
|
||||
];
|
||||
|
||||
case 'USER':
|
||||
@ -113,14 +120,17 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.createMarketDataOfOwnAssetProfile,
|
||||
permissions.createOrder,
|
||||
permissions.createOwnTag,
|
||||
permissions.createWatchlistItem,
|
||||
permissions.deleteAccess,
|
||||
permissions.deleteAccount,
|
||||
permissions.deleteAccountBalance,
|
||||
permissions.deleteAuthDevice,
|
||||
permissions.deleteOrder,
|
||||
permissions.deleteOwnUser,
|
||||
permissions.deleteWatchlistItem,
|
||||
permissions.readAiPrompt,
|
||||
permissions.readMarketDataOfOwnAssetProfile,
|
||||
permissions.readWatchlist,
|
||||
permissions.updateAccount,
|
||||
permissions.updateAuthDevice,
|
||||
permissions.updateMarketDataOfOwnAssetProfile,
|
||||
|
Loading…
x
Reference in New Issue
Block a user