Browse Source

Initial setup with lookup

pull/4016/head
Thomas Kaul 10 months ago
parent
commit
27fbc9ea6a
  1. 2
      apps/api/src/app/app.module.ts
  2. 43
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  3. 83
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts
  4. 93
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  5. 1
      apps/api/src/app/user/user.service.ts
  6. 3
      apps/api/src/services/configuration/configuration.service.ts
  7. 1
      apps/api/src/services/interfaces/environment.interface.ts
  8. 9
      apps/client/src/app/app-routing.module.ts
  9. 56
      apps/client/src/app/pages/api/api-page.component.ts
  10. 10
      apps/client/src/app/pages/api/api-page.html
  11. 3
      apps/client/src/app/pages/api/api-page.scss
  12. 1
      libs/common/src/lib/permissions.ts
  13. 2
      prisma/migrations/20241103110114_added_ghostfolio_to_data_source/migration.sql
  14. 1
      prisma/schema.prisma

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

@ -31,6 +31,7 @@ import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module'; import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { PublicModule } from './endpoints/public/public.module'; import { PublicModule } from './endpoints/public/public.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
@ -76,6 +77,7 @@ import { UserModule } from './user/user.module';
ExchangeRateModule, ExchangeRateModule,
ExchangeRateDataModule, ExchangeRateDataModule,
ExportModule, ExportModule,
GhostfolioModule,
HealthModule, HealthModule,
ImportModule, ImportModule,
InfoModule, InfoModule,

43
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -0,0 +1,43 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { permissions } from '@ghostfolio/common/permissions';
import {
Controller,
Get,
HttpException,
Query,
UseGuards
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { GhostfolioService } from './ghostfolio.service';
@Controller('data-providers/ghostfolio')
export class GhostfolioController {
public constructor(private readonly ghostfolioService: GhostfolioService) {}
@Get('lookup')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async lookupSymbol(
@Query('includeIndices') includeIndicesParam = 'false',
@Query('query') query = ''
): Promise<{ items: LookupItem[] }> {
const includeIndices = includeIndicesParam === 'true';
try {
return this.ghostfolioService.lookup({
includeIndices,
query: query.toLowerCase()
});
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
}

83
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts

@ -0,0 +1,83 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service';
import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-provider/financial-modeling-prep/financial-modeling-prep.service';
import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
import { GhostfolioController } from './ghostfolio.controller';
import { GhostfolioService } from './ghostfolio.service';
@Module({
controllers: [GhostfolioController],
imports: [
CryptocurrencyModule,
DataProviderModule,
MarketDataModule,
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolProfileModule
],
providers: [
AlphaVantageService,
CoinGeckoService,
ConfigurationService,
DataProviderService,
EodHistoricalDataService,
FinancialModelingPrepService,
GhostfolioService,
GoogleSheetsService,
ManualService,
RapidApiService,
YahooFinanceService,
YahooFinanceDataEnhancerService,
{
inject: [
AlphaVantageService,
CoinGeckoService,
EodHistoricalDataService,
FinancialModelingPrepService,
GoogleSheetsService,
ManualService,
RapidApiService,
YahooFinanceService
],
provide: 'DataProviderInterfaces',
useFactory: (
alphaVantageService,
coinGeckoService,
eodHistoricalDataService,
financialModelingPrepService,
googleSheetsService,
manualService,
rapidApiService,
yahooFinanceService
) => [
alphaVantageService,
coinGeckoService,
eodHistoricalDataService,
financialModelingPrepService,
googleSheetsService,
manualService,
rapidApiService,
yahooFinanceService
]
}
]
})
export class GhostfolioModule {}

93
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -0,0 +1,93 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
@Injectable()
export class GhostfolioService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService
) {}
public async lookup({
includeIndices = false,
query
}: {
includeIndices?: boolean;
query: string;
}): Promise<{ items: LookupItem[] }> {
const results: { items: LookupItem[] } = { items: [] };
if (!query) {
return results;
}
try {
let lookupItems: LookupItem[] = [];
const promises: Promise<{ items: LookupItem[] }>[] = [];
if (query?.length < 2) {
return { items: lookupItems };
}
for (const dataProviderService of this.getDataProviderServices()) {
promises.push(
dataProviderService.search({
includeIndices,
query
})
);
}
const searchResults = await Promise.all(promises);
searchResults.forEach(({ items }) => {
if (items?.length > 0) {
lookupItems = lookupItems.concat(items);
}
});
const filteredItems = lookupItems
.filter(({ currency }) => {
// Only allow symbols with supported currency
return currency ? true : false;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
})
.map((lookupItem) => {
lookupItem.dataProviderInfo = this.getDataProviderInfo();
lookupItem.dataSource = 'GHOSTFOLIO';
return lookupItem;
});
results.items = filteredItems;
return results;
} catch (error) {
Logger.error(error, 'GhostfolioService');
throw error;
}
}
private getDataProviderInfo(): DataProviderInfo {
return {
isPremium: true,
name: 'Ghostfolio Premium',
url: 'https://ghostfol.io'
};
}
private getDataProviderServices() {
return this.configurationService
.get('DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER')
.map((dataSource) => {
return this.dataProviderService.getDataProvider(DataSource[dataSource]);
});
}
}

1
apps/api/src/app/user/user.service.ts

@ -307,6 +307,7 @@ export class UserService {
// Reset holdings view mode // Reset holdings view mode
user.Settings.settings.holdingsViewMode = undefined; user.Settings.settings.holdingsViewMode = undefined;
} else if (user.subscription?.type === 'Premium') { } else if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.enableDataProviderGhostfolio);
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
currentPermissions = without( currentPermissions = without(

3
apps/api/src/services/configuration/configuration.service.ts

@ -35,6 +35,9 @@ export class ConfigurationService {
DATA_SOURCES: json({ DATA_SOURCES: json({
default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO] default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO]
}), }),
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: json({
default: []
}),
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }),
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),

1
apps/api/src/services/interfaces/environment.interface.ts

@ -15,6 +15,7 @@ export interface Environment extends CleanedEnvAccessors {
DATA_SOURCE_EXCHANGE_RATES: string; DATA_SOURCE_EXCHANGE_RATES: string;
DATA_SOURCE_IMPORT: string; DATA_SOURCE_IMPORT: string;
DATA_SOURCES: string[]; DATA_SOURCES: string[];
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[];
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
ENABLE_FEATURE_READ_ONLY_MODE: boolean; ENABLE_FEATURE_READ_ONLY_MODE: boolean;
ENABLE_FEATURE_SOCIAL_LOGIN: boolean; ENABLE_FEATURE_SOCIAL_LOGIN: boolean;

9
apps/client/src/app/app-routing.module.ts

@ -32,6 +32,15 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule) import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
}, },
{
canActivate: [AuthGuard],
loadComponent: () =>
import('./pages/api/api-page.component').then(
(c) => c.GfApiPageComponent
),
path: 'api',
title: 'Ghostfolio API'
},
{ {
path: 'auth', path: 'auth',
loadChildren: () => loadChildren: () =>

56
apps/client/src/app/pages/api/api-page.component.ts

@ -0,0 +1,56 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { CommonModule } from '@angular/common';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { map, Observable, Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule],
selector: 'gf-api-page',
standalone: true,
styleUrls: ['./api-page.scss'],
templateUrl: './api-page.html'
})
export class GfApiPageComponent implements OnInit {
public symbols$: Observable<LookupItem[]>;
private unsubscribeSubject = new Subject<void>();
public constructor(private http: HttpClient) {}
public ngOnInit() {
this.symbols$ = this.fetchSymbols({ query: 'apple' });
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchSymbols({
includeIndices = false,
query
}: {
includeIndices?: boolean;
query: string;
}) {
let params = new HttpParams().set('query', query);
if (includeIndices) {
params = params.append('includeIndices', includeIndices);
}
return this.http
.get<{
items: LookupItem[];
}>('/api/v1/data-providers/ghostfolio/lookup', { params })
.pipe(
map(({ items }) => {
return items;
}),
takeUntil(this.unsubscribeSubject)
);
}
}

10
apps/client/src/app/pages/api/api-page.html

@ -0,0 +1,10 @@
<div class="container">
<div>
<h2 class="text-center">Lookup</h2>
<ul *ngIf="symbols$ | async as symbols">
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
</div>
</div>

3
apps/client/src/app/pages/api/api-page.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

1
libs/common/src/lib/permissions.ts

@ -22,6 +22,7 @@ export const permissions = {
deletePlatform: 'deletePlatform', deletePlatform: 'deletePlatform',
deleteTag: 'deleteTag', deleteTag: 'deleteTag',
deleteUser: 'deleteUser', deleteUser: 'deleteUser',
enableDataProviderGhostfolio: 'enableDataProviderGhostfolio',
enableFearAndGreedIndex: 'enableFearAndGreedIndex', enableFearAndGreedIndex: 'enableFearAndGreedIndex',
enableImport: 'enableImport', enableImport: 'enableImport',
enableBlog: 'enableBlog', enableBlog: 'enableBlog',

2
prisma/migrations/20241103110114_added_ghostfolio_to_data_source/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "DataSource" ADD VALUE 'GHOSTFOLIO';

1
prisma/schema.prisma

@ -281,6 +281,7 @@ enum DataSource {
COINGECKO COINGECKO
EOD_HISTORICAL_DATA EOD_HISTORICAL_DATA
FINANCIAL_MODELING_PREP FINANCIAL_MODELING_PREP
GHOSTFOLIO
GOOGLE_SHEETS GOOGLE_SHEETS
MANUAL MANUAL
RAPID_API RAPID_API

Loading…
Cancel
Save