Feature/introduce isUsedByUsersWithSubscription flag (#3573)

pull/3574/head
Thomas Kaul 2 months ago committed by GitHub
parent d5c56fb16c
commit 43afb16808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -27,12 +27,13 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types'; import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { import {
AssetClass, AssetClass,
AssetSubClass, AssetSubClass,
DataSource, DataSource,
Prisma, Prisma,
PrismaClient,
Property, Property,
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
@ -212,98 +213,113 @@ export class AdminService {
} }
} }
let [assetProfiles, count] = await Promise.all([ const extendedPrismaClient = this.getExtendedPrismaClient();
this.prismaService.symbolProfile.findMany({
orderBy,
skip,
take,
where,
select: {
_count: {
select: { Order: true }
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
id: true,
name: true,
Order: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
scraperConfiguration: true,
sectors: true,
symbol: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
let marketData: AdminMarketDataItem[] = assetProfiles.map( try {
({ let [assetProfiles, count] = await Promise.all([
_count, extendedPrismaClient.symbolProfile.findMany({
assetClass, orderBy,
assetSubClass, skip,
comment, take,
countries, where,
currency, select: {
dataSource, _count: {
id, select: { Order: true }
name, },
Order, assetClass: true,
sectors, assetSubClass: true,
symbol comment: true,
}) => { countries: true,
const countriesCount = countries ? Object.keys(countries).length : 0; currency: true,
const marketDataItemCount = dataSource: true,
marketDataItems.find((marketDataItem) => { id: true,
return ( isUsedByUsersWithSubscription: true,
marketDataItem.dataSource === dataSource && name: true,
marketDataItem.symbol === symbol Order: {
); orderBy: [{ date: 'asc' }],
})?._count ?? 0; select: { date: true },
const sectorsCount = sectors ? Object.keys(sectors).length : 0; take: 1
},
scraperConfiguration: true,
sectors: true,
symbol: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
return { let marketData: AdminMarketDataItem[] = await Promise.all(
assetClass, assetProfiles.map(
assetSubClass, async ({
comment, _count,
currency, assetClass,
countriesCount, assetSubClass,
dataSource, comment,
id, countries,
name, currency,
symbol, dataSource,
marketDataItemCount, id,
sectorsCount, isUsedByUsersWithSubscription,
activitiesCount: _count.Order, name,
date: Order?.[0]?.date Order,
}; sectors,
} symbol
); }) => {
const countriesCount = countries
? Object.keys(countries).length
: 0;
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
return {
assetClass,
assetSubClass,
comment,
currency,
countriesCount,
dataSource,
id,
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.Order,
date: Order?.[0]?.date,
isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription
};
}
)
);
if (presetId) { if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') { if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => { marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0; return countriesCount === 0;
}); });
} else if (presetId === 'ETF_WITHOUT_SECTORS') { } else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => { marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0; return sectorsCount === 0;
}); });
}
count = marketData.length;
} }
count = marketData.length; return {
} count,
marketData
};
} finally {
await extendedPrismaClient.$disconnect();
return { Logger.debug('Disconnect extended prisma client', 'AdminService');
count, }
marketData
};
} }
public async getMarketDataBySymbol({ public async getMarketDataBySymbol({
@ -431,6 +447,52 @@ export class AdminService {
return response; return response;
} }
private getExtendedPrismaClient() {
Logger.debug('Connect extended prisma client', 'AdminService');
const symbolProfileExtension = Prisma.defineExtension((client) => {
return client.$extends({
result: {
symbolProfile: {
isUsedByUsersWithSubscription: {
compute: async ({ id }) => {
const { _count } =
await this.prismaService.symbolProfile.findUnique({
select: {
_count: {
select: {
Order: {
where: {
User: {
Subscription: {
some: {
expiresAt: {
gt: new Date()
}
}
}
}
}
}
}
}
},
where: {
id
}
});
return _count.Order > 0;
}
}
}
}
});
});
return new PrismaClient().$extends(symbolProfileExtension);
}
private async getMarketDataForCurrencies(): Promise<AdminMarketData> { private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
const marketDataItems = await this.prismaService.marketData.groupBy({ const marketDataItems = await this.prismaService.marketData.groupBy({
_count: true, _count: true,

@ -6,8 +6,14 @@ import {
ghostfolioScraperApiSymbolPrefix ghostfolioScraperApiSymbolPrefix
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces'; import {
Filter,
InfoItem,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
@ -97,22 +103,11 @@ export class AdminMarketDataComponent
new MatTableDataSource(); new MatTableDataSource();
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string; public deviceType: string;
public displayedColumns = [ public displayedColumns: string[] = [];
'select',
'nameWithSymbol',
'dataSource',
'assetClass',
'assetSubClass',
'date',
'activitiesCount',
'marketDataItemCount',
'sectorsCount',
'countriesCount',
'comment',
'actions'
];
public filters$ = new Subject<Filter[]>(); public filters$ = new Subject<Filter[]>();
public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix;
public hasPermissionForSubscription: boolean;
public info: InfoItem;
public isLoading = false; public isLoading = false;
public isUUID = isUUID; public isUUID = isUUID;
public placeholder = ''; public placeholder = '';
@ -134,6 +129,33 @@ export class AdminMarketDataComponent
private router: Router, private router: Router,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.displayedColumns = [
'select',
'nameWithSymbol',
'dataSource',
'assetClass',
'assetSubClass',
'date',
'activitiesCount',
'marketDataItemCount',
'sectorsCount',
'countriesCount'
];
if (this.hasPermissionForSubscription) {
this.displayedColumns.push('isUsedByUsersWithSubscription');
}
this.displayedColumns.push('comment');
this.displayedColumns.push('actions');
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => { .subscribe((params) => {

@ -144,6 +144,15 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="isUsedByUsersWithSubscription">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (element.isUsedByUsersWithSubscription) {
<gf-premium-indicator [enableLink]="false" />
}
</td>
</ng-container>
<ng-container matColumnDef="comment"> <ng-container matColumnDef="comment">
<th *matHeaderCellDef class="px-1" mat-header-cell></th> <th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>

@ -1,5 +1,6 @@
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter'; import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@ -24,6 +25,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
GfActivitiesFilterComponent, GfActivitiesFilterComponent,
GfAssetProfileDialogModule, GfAssetProfileDialogModule,
GfCreateAssetProfileDialogModule, GfCreateAssetProfileDialogModule,
GfPremiumIndicatorComponent,
GfSymbolModule, GfSymbolModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,

@ -15,6 +15,7 @@ export interface AdminMarketDataItem {
date: Date; date: Date;
id: string; id: string;
isBenchmark?: boolean; isBenchmark?: boolean;
isUsedByUsersWithSubscription?: boolean;
marketDataItemCount: number; marketDataItemCount: number;
name: string; name: string;
sectorsCount: number; sectorsCount: number;

Loading…
Cancel
Save