Browse Source

Validate tags when creating or updating an activity

pull/7347/head
Thomas Kaul 1 week ago
parent
commit
31a03d6987
  1. 3
      apps/api/src/app/account-balance/account-balance.module.ts
  2. 2
      apps/api/src/app/account/account.module.ts
  3. 35
      apps/api/src/app/account/account.service.ts
  4. 1
      apps/api/src/app/activities/activities.controller.ts
  5. 2
      apps/api/src/app/activities/activities.module.ts
  6. 27
      apps/api/src/app/activities/activities.service.ts
  7. 2
      apps/api/src/app/endpoints/ai/ai.module.ts
  8. 2
      apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts
  9. 2
      apps/api/src/app/endpoints/public/public.module.ts
  10. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  11. 2
      apps/api/src/app/portfolio/portfolio.module.ts
  12. 2
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  13. 25
      apps/api/src/services/tag/tag.service.ts

3
apps/api/src/app/account-balance/account-balance.module.ts

@ -1,6 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -10,7 +11,7 @@ import { AccountBalanceService } from './account-balance.service';
@Module({ @Module({
controllers: [AccountBalanceController], controllers: [AccountBalanceController],
exports: [AccountBalanceService], exports: [AccountBalanceService],
imports: [ExchangeRateDataModule, PrismaModule], imports: [ExchangeRateDataModule, PrismaModule, TagModule],
providers: [AccountBalanceService, AccountService] providers: [AccountBalanceService, AccountService]
}) })
export class AccountBalanceModule {} export class AccountBalanceModule {}

2
apps/api/src/app/account/account.module.ts

@ -7,6 +7,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -25,6 +26,7 @@ import { AccountService } from './account.service';
PortfolioModule, PortfolioModule,
PrismaModule, PrismaModule,
RedactValuesInResponseModule, RedactValuesInResponseModule,
TagModule,
UserModule UserModule
], ],
providers: [AccountService] providers: [AccountService]

35
apps/api/src/app/account/account.service.ts

@ -3,10 +3,11 @@ import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.
import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { import {
Account, Account,
@ -19,7 +20,6 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface'; import { CashDetails } from './interfaces/cash-details.interface';
@ -30,7 +30,8 @@ export class AccountService {
private readonly accountBalanceService: AccountBalanceService, private readonly accountBalanceService: AccountBalanceService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService private readonly prismaService: PrismaService,
private readonly tagService: TagService
) {} ) {}
public async account({ public async account({
@ -127,7 +128,7 @@ export class AccountService {
aUserId: string, aUserId: string,
tagIds?: string[] tagIds?: string[]
): Promise<Account> { ): Promise<Account> {
await this.validateTagIds(tagIds, aUserId); await this.tagService.validateTagIds(tagIds, aUserId);
const account = await this.prismaService.account.create({ const account = await this.prismaService.account.create({
data: { data: {
@ -259,15 +260,15 @@ export class AccountService {
public async updateAccount( public async updateAccount(
params: { params: {
where: Prisma.AccountWhereUniqueInput;
data: Prisma.AccountUpdateInput; data: Prisma.AccountUpdateInput;
where: Prisma.AccountWhereUniqueInput;
}, },
aUserId: string, aUserId: string,
tagIds?: string[] tagIds?: string[]
): Promise<Account> { ): Promise<Account> {
const { data, where } = params; const { data, where } = params;
await this.validateTagIds(tagIds, aUserId); await this.tagService.validateTagIds(tagIds, aUserId);
const account = await this.prismaService.account.update({ const account = await this.prismaService.account.update({
data: { data: {
@ -340,26 +341,4 @@ export class AccountService {
}); });
} }
} }
private async validateTagIds(tagIds: string[], userId: string) {
if (!tagIds?.length) {
return;
}
const uniqueTagIds = Array.from(new Set(tagIds));
const tagsCount = await this.prismaService.tag.count({
where: {
id: { in: uniqueTagIds },
OR: [{ userId }, { userId: null }]
}
});
if (tagsCount !== uniqueTagIds.length) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
} }

1
apps/api/src/app/activities/activities.controller.ts

@ -338,6 +338,7 @@ export class ActivitiesController {
}), }),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
userId: this.request.user.id,
where: { where: {
id id
} }

2
apps/api/src/app/activities/activities.module.ts

@ -14,6 +14,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -36,6 +37,7 @@ import { ActivitiesService } from './activities.service';
RedactValuesInResponseModule, RedactValuesInResponseModule,
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule,
TransformDataSourceInRequestModule, TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule TransformDataSourceInResponseModule
], ],

27
apps/api/src/app/activities/activities.service.ts

@ -12,6 +12,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
@ -61,7 +62,8 @@ export class ActivitiesService {
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService,
private readonly tagService: TagService
) {} ) {}
public areCashActivitiesExcludedByFilters(filters: Filter[] = []) { public areCashActivitiesExcludedByFilters(filters: Filter[] = []) {
@ -153,6 +155,15 @@ export class ActivitiesService {
userId: string; userId: string;
} }
): Promise<Order> { ): Promise<Order> {
const tags = data.tags ?? [];
await this.tagService.validateTagIds(
tags.map(({ id }) => {
return id;
}),
data.userId
);
let account: Prisma.AccountCreateNestedOneWithoutActivitiesInput; let account: Prisma.AccountCreateNestedOneWithoutActivitiesInput;
if (data.accountId) { if (data.accountId) {
@ -167,7 +178,6 @@ export class ActivitiesService {
} }
const accountId = data.accountId; const accountId = data.accountId;
const tags = data.tags ?? [];
const updateAccountBalance = data.updateAccountBalance ?? false; const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId; const userId = data.userId;
@ -932,6 +942,7 @@ export class ActivitiesService {
public async updateActivity({ public async updateActivity({
data, data,
userId,
where where
}: { }: {
data: Prisma.OrderUpdateInput & { data: Prisma.OrderUpdateInput & {
@ -942,14 +953,22 @@ export class ActivitiesService {
tags?: { id: string }[]; tags?: { id: string }[];
type?: ActivityType; type?: ActivityType;
}; };
userId: string;
where: Prisma.OrderWhereUniqueInput; where: Prisma.OrderWhereUniqueInput;
}): Promise<Order> { }): Promise<Order> {
const tags = data.tags ?? [];
await this.tagService.validateTagIds(
tags.map(({ id }) => {
return id;
}),
userId
);
if (!data.comment) { if (!data.comment) {
data.comment = null; data.comment = null;
} }
const tags = data.tags ?? [];
let isDraft = false; let isDraft = false;
if ( if (

2
apps/api/src/app/endpoints/ai/ai.module.ts

@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -44,6 +45,7 @@ import { AiService } from './ai.service';
PropertyModule, PropertyModule,
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule,
UserModule UserModule
], ],
providers: [ providers: [

2
apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts

@ -23,6 +23,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -46,6 +47,7 @@ import { BenchmarksService } from './benchmarks.service';
RedisCacheModule, RedisCacheModule,
SymbolModule, SymbolModule,
SymbolProfileModule, SymbolProfileModule,
TagModule,
TransformDataSourceInRequestModule, TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule, TransformDataSourceInResponseModule,
UserModule UserModule

2
apps/api/src/app/endpoints/public/public.module.ts

@ -18,6 +18,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -38,6 +39,7 @@ import { PublicController } from './public.controller';
PrismaModule, PrismaModule,
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule,
TransformDataSourceInRequestModule, TransformDataSourceInRequestModule,
UserModule UserModule
], ],

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -91,6 +91,7 @@ describe('PortfolioCalculator', () => {
accountBalanceService, accountBalanceService,
null, null,
exchangeRateDataService, exchangeRateDataService,
null,
null null
); );
@ -122,6 +123,7 @@ describe('PortfolioCalculator', () => {
exchangeRateDataService, exchangeRateDataService,
null, null,
null, null,
null,
null null
); );

2
apps/api/src/app/portfolio/portfolio.module.ts

@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -50,6 +51,7 @@ import { RulesService } from './rules.service';
RedactValuesInResponseModule, RedactValuesInResponseModule,
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule,
TransformDataSourceInRequestModule, TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule, TransformDataSourceInResponseModule,
UserModule UserModule

2
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -54,6 +54,7 @@ describe('PortfolioService', () => {
null, null,
null, null,
exchangeRateDataService, exchangeRateDataService,
null,
null null
); );
@ -67,6 +68,7 @@ describe('PortfolioService', () => {
exchangeRateDataService, exchangeRateDataService,
null, null,
null, null,
null,
null null
); );

25
apps/api/src/services/tag/tag.service.ts

@ -1,7 +1,8 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { Prisma, Tag } from '@prisma/client'; import { Prisma, Tag } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable() @Injectable()
export class TagService { export class TagService {
@ -124,4 +125,26 @@ export class TagService {
where where
}); });
} }
public async validateTagIds(tagIds: string[], userId: string) {
if (!tagIds?.length) {
return;
}
const uniqueTagIds = Array.from(new Set(tagIds));
const tagsCount = await this.prismaService.tag.count({
where: {
id: { in: uniqueTagIds },
OR: [{ userId }, { userId: null }]
}
});
if (tagsCount !== uniqueTagIds.length) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
} }

Loading…
Cancel
Save