Feature/set up event system for portfolio changes (#3333)

* Set up event system for portfolio changes

* Update changelog
pull/3336/head
Thomas Kaul 7 months ago committed by GitHub
parent a4efbc0131
commit b692b7432c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Extended the content of the _Self-Hosting_ section by the custom asset instructions on the Frequently Asked Questions (FAQ) page
- Set up an event system to follow portfolio changes
### Changed

@ -1,7 +1,6 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { resetHours } from '@ghostfolio/common/helper';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -18,7 +17,6 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { AccountBalance } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountBalanceService } from './account-balance.service';
@ -67,10 +65,11 @@ export class AccountBalanceController {
@Param('id') id: string
): Promise<AccountBalance> {
const accountBalance = await this.accountBalanceService.accountBalance({
id
id,
userId: this.request.user.id
});
if (!accountBalance || accountBalance.userId !== this.request.user.id) {
if (!accountBalance) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
@ -78,7 +77,8 @@ export class AccountBalanceController {
}
return this.accountBalanceService.deleteAccountBalance({
id
id: accountBalance.id,
userId: accountBalance.userId
});
}
}

@ -1,3 +1,4 @@
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper';
@ -5,6 +6,7 @@ import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AccountBalance, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns';
@ -13,6 +15,7 @@ import { CreateAccountBalanceDto } from './create-account-balance.dto';
@Injectable()
export class AccountBalanceService {
public constructor(
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
@ -36,7 +39,7 @@ export class AccountBalanceService {
}: CreateAccountBalanceDto & {
userId: string;
}): Promise<AccountBalance> {
return this.prismaService.accountBalance.upsert({
const accountBalance = await this.prismaService.accountBalance.upsert({
create: {
Account: {
connect: {
@ -59,14 +62,32 @@ export class AccountBalanceService {
}
}
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId
})
);
return accountBalance;
}
public async deleteAccountBalance(
where: Prisma.AccountBalanceWhereUniqueInput
): Promise<AccountBalance> {
return this.prismaService.accountBalance.delete({
const accountBalance = await this.prismaService.accountBalance.delete({
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: <string>where.userId
})
);
return accountBalance;
}
public async getAccountBalances({

@ -1,10 +1,12 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Account, Order, Platform, Prisma } from '@prisma/client';
import { Big } from 'big.js';
import { format } from 'date-fns';
@ -16,6 +18,7 @@ import { CashDetails } from './interfaces/cash-details.interface';
export class AccountService {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
@ -94,6 +97,13 @@ export class AccountService {
userId: aUserId
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
@ -101,9 +111,18 @@ export class AccountService {
where: Prisma.AccountWhereUniqueInput,
aUserId: string
): Promise<Account> {
return this.prismaService.account.delete({
const account = await this.prismaService.account.delete({
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async getAccounts(aUserId: string): Promise<Account[]> {
@ -201,10 +220,19 @@ export class AccountService {
userId: aUserId
});
return this.prismaService.account.update({
const account = await this.prismaService.account.update({
data,
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: account.userId
})
);
return account;
}
public async updateAccountBalance({

@ -1,3 +1,4 @@
import { EventsModule } from '@ghostfolio/api/events/events.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { CronService } from '@ghostfolio/api/services/cron.service';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
@ -14,6 +15,7 @@ import {
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { StatusCodes } from 'http-status-codes';
@ -44,6 +46,7 @@ import { TagModule } from './tag/tag.module';
import { UserModule } from './user/user.module';
@Module({
controllers: [AppController],
imports: [
AdminModule,
AccessModule,
@ -64,6 +67,8 @@ import { UserModule } from './user/user.module';
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
EventEmitterModule.forRoot(),
EventsModule,
ExchangeRateModule,
ExchangeRateDataModule,
ExportModule,
@ -109,7 +114,6 @@ import { UserModule } from './user/user.module';
TwitterBotModule,
UserModule
],
controllers: [AppController],
providers: [CronService]
})
export class AppModule {}

@ -60,15 +60,15 @@ export class OrderController {
}
@Delete(':id')
@HasPermission(permissions.deleteOrder)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteOrder(@Param('id') id: string): Promise<OrderModel> {
const order = await this.orderService.order({ id });
const order = await this.orderService.order({
id,
userId: this.request.user.id
});
if (
!hasPermission(this.request.user.permissions, permissions.deleteOrder) ||
!order ||
order.userId !== this.request.user.id
) {
if (!order) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN

@ -1,4 +1,5 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -13,6 +14,7 @@ import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
AssetClass,
AssetSubClass,
@ -27,7 +29,6 @@ import { endOfToday, isAfter } from 'date-fns';
import { groupBy, uniqBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { CreateOrderDto } from './create-order.dto';
import { Activities } from './interfaces/activities.interface';
@Injectable()
@ -35,6 +36,7 @@ export class OrderService {
public constructor(
private readonly accountService: AccountService,
private readonly dataGatheringService: DataGatheringService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
@ -160,6 +162,13 @@ export class OrderService {
});
}
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: order.userId
})
);
return order;
}
@ -174,6 +183,13 @@ export class OrderService {
await this.symbolProfileService.deleteById(order.symbolProfileId);
}
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: order.userId
})
);
return order;
}
@ -182,6 +198,13 @@ export class OrderService {
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: <string>where.userId
})
);
return count;
}
@ -455,7 +478,7 @@ export class OrderService {
where
});
return this.prismaService.order.update({
const order = await this.prismaService.order.update({
data: {
...data,
isDraft,
@ -467,6 +490,15 @@ export class OrderService {
},
where
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: order.userId
})
);
return order;
}
private async orders(params: {

@ -1,5 +1,6 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -25,6 +26,7 @@ import {
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, Role, User } from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { sortBy, without } from 'lodash';
@ -37,6 +39,7 @@ export class UserService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly eventEmitter: EventEmitter2,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService,
@ -437,11 +440,9 @@ export class UserService {
userId: string;
userSettings: UserSettings;
}) {
const settings = userSettings as unknown as Prisma.JsonObject;
await this.prismaService.settings.upsert({
const { settings } = await this.prismaService.settings.upsert({
create: {
settings,
settings: userSettings as unknown as Prisma.JsonObject,
User: {
connect: {
id: userId
@ -449,14 +450,21 @@ export class UserService {
}
},
update: {
settings
settings: userSettings as unknown as Prisma.JsonObject
},
where: {
userId
}
});
return;
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId
})
);
return settings;
}
private getRandomString(length: number) {

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { PortfolioChangedListener } from './portfolio-changed.listener';
@Module({
providers: [PortfolioChangedListener]
})
export class EventsModule {}

@ -0,0 +1,15 @@
export class PortfolioChangedEvent {
private userId: string;
public constructor({ userId }: { userId: string }) {
this.userId = userId;
}
public static getName() {
return 'portfolio.changed';
}
public getUserId() {
return this.userId;
}
}

@ -0,0 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { PortfolioChangedEvent } from './portfolio-changed.event';
@Injectable()
export class PortfolioChangedListener {
@OnEvent(PortfolioChangedEvent.getName())
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
Logger.log(
`Portfolio of user with id ${event.getUserId()} has changed`,
'PortfolioChangedListener'
);
}
}

@ -78,6 +78,7 @@
"@nestjs/common": "10.1.3",
"@nestjs/config": "3.0.0",
"@nestjs/core": "10.1.3",
"@nestjs/event-emitter": "2.0.4",
"@nestjs/jwt": "10.1.0",
"@nestjs/passport": "10.0.0",
"@nestjs/platform-express": "10.1.3",

@ -4814,6 +4814,13 @@
path-to-regexp "3.2.0"
tslib "2.6.1"
"@nestjs/event-emitter@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz#de7d3986cfeb82639bb95181fab4fe5525437c74"
integrity sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==
dependencies:
eventemitter2 "6.4.9"
"@nestjs/jwt@10.1.0":
version "10.1.0"
resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-10.1.0.tgz#7899b68d68b998cc140bc0af392c63fc00755236"
@ -11653,7 +11660,7 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter2@^6.4.2:
eventemitter2@6.4.9, eventemitter2@^6.4.2:
version "6.4.9"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125"
integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==

Loading…
Cancel
Save