Browse Source

Transform data source (#658)

* Transform data source

* Update changelog
pull/662/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
155c08d665
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 19
      apps/api/src/app/order/order.controller.ts
  3. 79
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 14
      apps/api/src/app/subscription/subscription.controller.ts
  5. 8
      apps/api/src/app/symbol/symbol.controller.ts
  6. 45
      apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts
  7. 76
      apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts

5
CHANGELOG.md

@ -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) `accountId` in the import functionality for activities
- Added support for the (optional) `dataSource` 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`) - Added support for the cryptocurrency _Mina Protocol_ (`MINA-USD`)
### Changed ### 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` - Improved the consistent use of `symbol` in combination with `dataSource`
- Removed the primary data source from the client - Removed the primary data source from the client
### Removed
- Removed the unused endpoint `GET api/order/:id`
## 1.108.0 - 27.01.2022 ## 1.108.0 - 27.01.2022
### Changed ### Changed

19
apps/api/src/app/order/order.controller.ts

@ -1,5 +1,7 @@
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; 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 { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -14,7 +16,8 @@ import {
Param, Param,
Post, Post,
Put, Put,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -58,6 +61,7 @@ export class OrderController {
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders( public async getAllOrders(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<Activities> { ): Promise<Activities> {
@ -91,19 +95,9 @@ export class OrderController {
return { activities }; 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() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> { public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> {
if ( if (
!hasPermission(this.request.user.permissions, permissions.createOrder) !hasPermission(this.request.user.permissions, permissions.createOrder)
@ -138,6 +132,7 @@ export class OrderController {
@Put(':id') @Put(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) {
if ( if (
!hasPermission(this.request.user.permissions, permissions.updateOrder) !hasPermission(this.request.user.permissions, permissions.updateOrder)

79
apps/api/src/app/portfolio/portfolio.controller.ts

@ -4,9 +4,12 @@ import {
hasNotDefinedValuesInObject, hasNotDefinedValuesInObject,
nullifyValuesInObject nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper'; } 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 { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { baseCurrency } from '@ghostfolio/common/config'; import { baseCurrency } from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
import { import {
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
@ -25,13 +28,11 @@ import {
Inject, Inject,
Param, Param,
Query, Query,
Res, UseGuards,
UseGuards UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
@ -53,8 +54,7 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getChart( public async getChart(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range
@Res() res: Response
): Promise<PortfolioChart> { ): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioServiceStrategy const historicalDataContainer = await this.portfolioServiceStrategy
.get() .get()
@ -90,27 +90,29 @@ export class PortfolioController {
}); });
} }
return <any>res.json({ return {
hasError, hasError,
chart: chartData, chart: chartData,
isAllTimeHigh: historicalDataContainer.isAllTimeHigh, isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
isAllTimeLow: historicalDataContainer.isAllTimeLow isAllTimeLow: historicalDataContainer.isAllTimeLow
}); };
} }
@Get('details') @Get('details')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails( public async getDetails(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range
@Res() res: Response ): Promise<PortfolioDetails & { hasError: boolean }> {
): Promise<PortfolioDetails> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === 'Basic'
) { ) {
res.status(StatusCodes.FORBIDDEN); throw new HttpException(
return <any>res.json({ accounts: {}, holdings: {} }); getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
} }
let hasError = false; let hasError = false;
@ -159,21 +161,22 @@ export class PortfolioController {
} }
} }
return <any>res.json({ accounts, hasError, holdings }); return { accounts, hasError, holdings };
} }
@Get('investments') @Get('investments')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getInvestments( public async getInvestments(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string
@Res() res: Response
): Promise<PortfolioInvestments> { ): Promise<PortfolioInvestments> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === 'Basic'
) { ) {
res.status(StatusCodes.FORBIDDEN); throw new HttpException(
return <any>res.json({}); getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
} }
let investments = await this.portfolioServiceStrategy 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') @Get('performance')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPerformance( public async getPerformance(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range
@Res() res: Response
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
const performanceInformation = await this.portfolioServiceStrategy const performanceInformation = await this.portfolioServiceStrategy
.get() .get()
@ -219,15 +221,15 @@ export class PortfolioController {
); );
} }
return <any>res.json(performanceInformation); return performanceInformation;
} }
@Get('positions') @Get('positions')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions( public async getPositions(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('range') range
@Res() res: Response
): Promise<PortfolioPositions> { ): Promise<PortfolioPositions> {
const result = await this.portfolioServiceStrategy const result = await this.portfolioServiceStrategy
.get() .get()
@ -247,13 +249,12 @@ export class PortfolioController {
}); });
} }
return <any>res.json(result); return result;
} }
@Get('public/:accessId') @Get('public/:accessId')
public async getPublic( public async getPublic(
@Param('accessId') accessId, @Param('accessId') accessId
@Res() res: Response
): Promise<PortfolioPublicDetails> { ): Promise<PortfolioPublicDetails> {
const access = await this.accessService.access({ id: accessId }); const access = await this.accessService.access({ id: accessId });
const user = await this.userService.user({ const user = await this.userService.user({
@ -261,8 +262,10 @@ export class PortfolioController {
}); });
if (!access) { if (!access) {
res.status(StatusCodes.NOT_FOUND); throw new HttpException(
return <any>res.json({ accounts: {}, holdings: {} }); getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
} }
let hasDetails = true; let hasDetails = true;
@ -305,7 +308,7 @@ export class PortfolioController {
} }
} }
return <any>res.json(portfolioPublicDetails); return portfolioPublicDetails;
} }
@Get('summary') @Get('summary')
@ -339,6 +342,7 @@ export class PortfolioController {
} }
@Get('position/:dataSource/:symbol') @Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPosition( public async getPosition(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@ -376,21 +380,18 @@ export class PortfolioController {
@Get('report') @Get('report')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getReport( public async getReport(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string
@Res() res: Response
): Promise<PortfolioReport> { ): Promise<PortfolioReport> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === 'Basic'
) { ) {
res.status(StatusCodes.FORBIDDEN); throw new HttpException(
return <any>res.json({ rules: [] }); getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
} }
return <any>( return await this.portfolioServiceStrategy.get().getReport(impersonationId);
res.json(
await this.portfolioServiceStrategy.get().getReport(impersonationId)
)
);
} }
} }

14
apps/api/src/app/subscription/subscription.controller.ts

@ -7,6 +7,7 @@ import {
Body, Body,
Controller, Controller,
Get, Get,
HttpCode,
HttpException, HttpException,
Inject, Inject,
Logger, Logger,
@ -17,7 +18,6 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { SubscriptionService } from './subscription.service'; import { SubscriptionService } from './subscription.service';
@ -32,11 +32,9 @@ export class SubscriptionController {
) {} ) {}
@Post('redeem-coupon') @Post('redeem-coupon')
@HttpCode(StatusCodes.OK)
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async redeemCoupon( public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) {
@Body() { couponCode }: { couponCode: string },
@Res() res: Response
) {
if (!this.request.user) { if (!this.request.user) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
@ -74,12 +72,10 @@ export class SubscriptionController {
`Subscription for user '${this.request.user.id}' has been created with coupon` `Subscription for user '${this.request.user.id}' has been created with coupon`
); );
res.status(StatusCodes.OK); return {
return <any>res.json({
message: getReasonPhrase(StatusCodes.OK), message: getReasonPhrase(StatusCodes.OK),
statusCode: StatusCodes.OK statusCode: StatusCodes.OK
}); };
} }
@Get('stripe/callback') @Get('stripe/callback')

8
apps/api/src/app/symbol/symbol.controller.ts

@ -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 { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
Controller, Controller,
@ -5,7 +7,8 @@ import {
HttpException, HttpException,
Param, Param,
Query, Query,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -25,6 +28,7 @@ export class SymbolController {
*/ */
@Get('lookup') @Get('lookup')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async lookupSymbol( public async lookupSymbol(
@Query() { query = '' } @Query() { query = '' }
): Promise<{ items: LookupItem[] }> { ): Promise<{ items: LookupItem[] }> {
@ -43,6 +47,8 @@ export class SymbolController {
*/ */
@Get(':dataSource/:symbol') @Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getSymbolData( public async getSymbolData(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string, @Param('symbol') symbol: string,

45
apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts

@ -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();
}
}

76
apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts

@ -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…
Cancel
Save