Transform data source (#658)
* Transform data source * Update changelog
This commit is contained in:
parent
b8ad6d6662
commit
155c08d665
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Added support for the (optional) `accountId` in the import functionality for activities
|
||||
- Added support for the (optional) `dataSource` in the import functionality for activities
|
||||
- Added support for the data source transformation
|
||||
- Added support for the cryptocurrency _Mina Protocol_ (`MINA-USD`)
|
||||
|
||||
### Changed
|
||||
@ -18,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improved the consistent use of `symbol` in combination with `dataSource`
|
||||
- Removed the primary data source from the client
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the unused endpoint `GET api/order/:id`
|
||||
|
||||
## 1.108.0 - 27.01.2022
|
||||
|
||||
### Changed
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
@ -14,7 +16,8 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
@ -58,6 +61,7 @@ export class OrderController {
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getAllOrders(
|
||||
@Headers('impersonation-id') impersonationId
|
||||
): Promise<Activities> {
|
||||
@ -91,19 +95,9 @@ export class OrderController {
|
||||
return { activities };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getOrderById(@Param('id') id: string): Promise<OrderModel> {
|
||||
return this.orderService.order({
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> {
|
||||
if (
|
||||
!hasPermission(this.request.user.permissions, permissions.createOrder)
|
||||
@ -138,6 +132,7 @@ export class OrderController {
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) {
|
||||
if (
|
||||
!hasPermission(this.request.user.permissions, permissions.updateOrder)
|
||||
|
@ -4,9 +4,12 @@ import {
|
||||
hasNotDefinedValuesInObject,
|
||||
nullifyValuesInObject
|
||||
} from '@ghostfolio/api/helper/object.helper';
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
@ -25,13 +28,11 @@ import {
|
||||
Inject,
|
||||
Param,
|
||||
Query,
|
||||
Res,
|
||||
UseGuards
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { Response } from 'express';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||
@ -53,8 +54,7 @@ export class PortfolioController {
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getChart(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') range,
|
||||
@Res() res: Response
|
||||
@Query('range') range
|
||||
): Promise<PortfolioChart> {
|
||||
const historicalDataContainer = await this.portfolioServiceStrategy
|
||||
.get()
|
||||
@ -90,27 +90,29 @@ export class PortfolioController {
|
||||
});
|
||||
}
|
||||
|
||||
return <any>res.json({
|
||||
return {
|
||||
hasError,
|
||||
chart: chartData,
|
||||
isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
|
||||
isAllTimeLow: historicalDataContainer.isAllTimeLow
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Get('details')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getDetails(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') range,
|
||||
@Res() res: Response
|
||||
): Promise<PortfolioDetails> {
|
||||
@Query('range') range
|
||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
this.request.user.subscription.type === 'Basic'
|
||||
) {
|
||||
res.status(StatusCodes.FORBIDDEN);
|
||||
return <any>res.json({ accounts: {}, holdings: {} });
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
@ -159,21 +161,22 @@ export class PortfolioController {
|
||||
}
|
||||
}
|
||||
|
||||
return <any>res.json({ accounts, hasError, holdings });
|
||||
return { accounts, hasError, holdings };
|
||||
}
|
||||
|
||||
@Get('investments')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getInvestments(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Res() res: Response
|
||||
@Headers('impersonation-id') impersonationId: string
|
||||
): Promise<PortfolioInvestments> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
this.request.user.subscription.type === 'Basic'
|
||||
) {
|
||||
res.status(StatusCodes.FORBIDDEN);
|
||||
return <any>res.json({});
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
let investments = await this.portfolioServiceStrategy
|
||||
@ -195,15 +198,14 @@ export class PortfolioController {
|
||||
}));
|
||||
}
|
||||
|
||||
return <any>res.json({ firstOrderDate: investments[0]?.date, investments });
|
||||
return { firstOrderDate: parseDate(investments[0]?.date), investments };
|
||||
}
|
||||
|
||||
@Get('performance')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getPerformance(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') range,
|
||||
@Res() res: Response
|
||||
@Query('range') range
|
||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
||||
const performanceInformation = await this.portfolioServiceStrategy
|
||||
.get()
|
||||
@ -219,15 +221,15 @@ export class PortfolioController {
|
||||
);
|
||||
}
|
||||
|
||||
return <any>res.json(performanceInformation);
|
||||
return performanceInformation;
|
||||
}
|
||||
|
||||
@Get('positions')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getPositions(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') range,
|
||||
@Res() res: Response
|
||||
@Query('range') range
|
||||
): Promise<PortfolioPositions> {
|
||||
const result = await this.portfolioServiceStrategy
|
||||
.get()
|
||||
@ -247,13 +249,12 @@ export class PortfolioController {
|
||||
});
|
||||
}
|
||||
|
||||
return <any>res.json(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('public/:accessId')
|
||||
public async getPublic(
|
||||
@Param('accessId') accessId,
|
||||
@Res() res: Response
|
||||
@Param('accessId') accessId
|
||||
): Promise<PortfolioPublicDetails> {
|
||||
const access = await this.accessService.access({ id: accessId });
|
||||
const user = await this.userService.user({
|
||||
@ -261,8 +262,10 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (!access) {
|
||||
res.status(StatusCodes.NOT_FOUND);
|
||||
return <any>res.json({ accounts: {}, holdings: {} });
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||
StatusCodes.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
let hasDetails = true;
|
||||
@ -305,7 +308,7 @@ export class PortfolioController {
|
||||
}
|
||||
}
|
||||
|
||||
return <any>res.json(portfolioPublicDetails);
|
||||
return portfolioPublicDetails;
|
||||
}
|
||||
|
||||
@Get('summary')
|
||||
@ -339,6 +342,7 @@ export class PortfolioController {
|
||||
}
|
||||
|
||||
@Get('position/:dataSource/:symbol')
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getPosition(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@ -376,21 +380,18 @@ export class PortfolioController {
|
||||
@Get('report')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getReport(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Res() res: Response
|
||||
@Headers('impersonation-id') impersonationId: string
|
||||
): Promise<PortfolioReport> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
this.request.user.subscription.type === 'Basic'
|
||||
) {
|
||||
res.status(StatusCodes.FORBIDDEN);
|
||||
return <any>res.json({ rules: [] });
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return <any>(
|
||||
res.json(
|
||||
await this.portfolioServiceStrategy.get().getReport(impersonationId)
|
||||
)
|
||||
);
|
||||
return await this.portfolioServiceStrategy.get().getReport(impersonationId);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpException,
|
||||
Inject,
|
||||
Logger,
|
||||
@ -17,7 +18,6 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Response } from 'express';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { SubscriptionService } from './subscription.service';
|
||||
@ -32,11 +32,9 @@ export class SubscriptionController {
|
||||
) {}
|
||||
|
||||
@Post('redeem-coupon')
|
||||
@HttpCode(StatusCodes.OK)
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async redeemCoupon(
|
||||
@Body() { couponCode }: { couponCode: string },
|
||||
@Res() res: Response
|
||||
) {
|
||||
public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) {
|
||||
if (!this.request.user) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
@ -74,12 +72,10 @@ export class SubscriptionController {
|
||||
`Subscription for user '${this.request.user.id}' has been created with coupon`
|
||||
);
|
||||
|
||||
res.status(StatusCodes.OK);
|
||||
|
||||
return <any>res.json({
|
||||
return {
|
||||
message: getReasonPhrase(StatusCodes.OK),
|
||||
statusCode: StatusCodes.OK
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Get('stripe/callback')
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import {
|
||||
Controller,
|
||||
@ -5,7 +7,8 @@ import {
|
||||
HttpException,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource } from '@prisma/client';
|
||||
@ -25,6 +28,7 @@ export class SymbolController {
|
||||
*/
|
||||
@Get('lookup')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async lookupSymbol(
|
||||
@Query() { query = '' }
|
||||
): Promise<{ items: LookupItem[] }> {
|
||||
@ -43,6 +47,8 @@ export class SymbolController {
|
||||
*/
|
||||
@Get(':dataSource/:symbol')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getSymbolData(
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('symbol') symbol: string,
|
||||
|
@ -0,0 +1,45 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ConfigurationService } from '../services/configuration.service';
|
||||
|
||||
@Injectable()
|
||||
export class TransformDataSourceInRequestInterceptor<T>
|
||||
implements NestInterceptor<T, any>
|
||||
{
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {}
|
||||
|
||||
public intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler<T>
|
||||
): Observable<any> {
|
||||
const http = context.switchToHttp();
|
||||
const request = http.getRequest();
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) {
|
||||
if (request.body.dataSource) {
|
||||
request.body.dataSource = this.decodeDataSource(
|
||||
request.body.dataSource
|
||||
);
|
||||
}
|
||||
|
||||
if (request.params.dataSource) {
|
||||
request.params.dataSource = this.decodeDataSource(
|
||||
request.params.dataSource
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
private decodeDataSource(encodeDataSource: string) {
|
||||
return Buffer.from(encodeDataSource, 'hex').toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor
|
||||
} from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ConfigurationService } from '../services/configuration.service';
|
||||
|
||||
@Injectable()
|
||||
export class TransformDataSourceInResponseInterceptor<T>
|
||||
implements NestInterceptor<T, any>
|
||||
{
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {}
|
||||
|
||||
public intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler<T>
|
||||
): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data: any) => {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true
|
||||
) {
|
||||
if (data.activities) {
|
||||
data.activities.map((activity) => {
|
||||
activity.SymbolProfile.dataSource = this.encodeDataSource(
|
||||
activity.SymbolProfile.dataSource
|
||||
);
|
||||
activity.dataSource = this.encodeDataSource(activity.dataSource);
|
||||
return activity;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.dataSource) {
|
||||
data.dataSource = this.encodeDataSource(data.dataSource);
|
||||
}
|
||||
|
||||
if (data.holdings) {
|
||||
for (const symbol of Object.keys(data.holdings)) {
|
||||
if (data.holdings[symbol].dataSource) {
|
||||
data.holdings[symbol].dataSource = this.encodeDataSource(
|
||||
data.holdings[symbol].dataSource
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.items) {
|
||||
data.items.map((item) => {
|
||||
item.dataSource = this.encodeDataSource(item.dataSource);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.positions) {
|
||||
data.positions.map((position) => {
|
||||
position.dataSource = this.encodeDataSource(position.dataSource);
|
||||
return position;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private encodeDataSource(aDataSource: DataSource) {
|
||||
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user