Feature/introduce isUsedByUsersWithSubscription flag (#3573)
This commit is contained in:
@@ -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,
|
try {
|
||||||
skip,
|
let [assetProfiles, count] = await Promise.all([
|
||||||
take,
|
extendedPrismaClient.symbolProfile.findMany({
|
||||||
where,
|
orderBy,
|
||||||
select: {
|
skip,
|
||||||
_count: {
|
take,
|
||||||
select: { Order: true }
|
where,
|
||||||
},
|
select: {
|
||||||
assetClass: true,
|
_count: {
|
||||||
assetSubClass: true,
|
select: { Order: true }
|
||||||
comment: true,
|
},
|
||||||
countries: true,
|
assetClass: true,
|
||||||
currency: true,
|
assetSubClass: true,
|
||||||
dataSource: true,
|
comment: true,
|
||||||
id: true,
|
countries: true,
|
||||||
name: true,
|
currency: true,
|
||||||
Order: {
|
dataSource: true,
|
||||||
orderBy: [{ date: 'asc' }],
|
id: true,
|
||||||
select: { date: true },
|
isUsedByUsersWithSubscription: true,
|
||||||
take: 1
|
name: true,
|
||||||
},
|
Order: {
|
||||||
scraperConfiguration: true,
|
orderBy: [{ date: 'asc' }],
|
||||||
sectors: true,
|
select: { date: true },
|
||||||
symbol: true
|
take: 1
|
||||||
|
},
|
||||||
|
scraperConfiguration: true,
|
||||||
|
sectors: true,
|
||||||
|
symbol: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.prismaService.symbolProfile.count({ where })
|
||||||
|
]);
|
||||||
|
|
||||||
|
let marketData: AdminMarketDataItem[] = await Promise.all(
|
||||||
|
assetProfiles.map(
|
||||||
|
async ({
|
||||||
|
_count,
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
comment,
|
||||||
|
countries,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
id,
|
||||||
|
isUsedByUsersWithSubscription,
|
||||||
|
name,
|
||||||
|
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 === 'ETF_WITHOUT_COUNTRIES') {
|
||||||
|
marketData = marketData.filter(({ countriesCount }) => {
|
||||||
|
return countriesCount === 0;
|
||||||
|
});
|
||||||
|
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
|
||||||
|
marketData = marketData.filter(({ sectorsCount }) => {
|
||||||
|
return sectorsCount === 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
this.prismaService.symbolProfile.count({ where })
|
|
||||||
]);
|
|
||||||
|
|
||||||
let marketData: AdminMarketDataItem[] = assetProfiles.map(
|
count = marketData.length;
|
||||||
({
|
|
||||||
_count,
|
|
||||||
assetClass,
|
|
||||||
assetSubClass,
|
|
||||||
comment,
|
|
||||||
countries,
|
|
||||||
currency,
|
|
||||||
dataSource,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (presetId) {
|
|
||||||
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
|
|
||||||
marketData = marketData.filter(({ countriesCount }) => {
|
|
||||||
return countriesCount === 0;
|
|
||||||
});
|
|
||||||
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
|
|
||||||
marketData = marketData.filter(({ sectorsCount }) => {
|
|
||||||
return sectorsCount === 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count = marketData.length;
|
return {
|
||||||
|
count,
|
||||||
|
marketData
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await extendedPrismaClient.$disconnect();
|
||||||
|
|
||||||
|
Logger.debug('Disconnect extended prisma client', 'AdminService');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
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;
|
||||||
|
Reference in New Issue
Block a user