mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
128 changed files with 8443 additions and 3203 deletions
@ -0,0 +1,11 @@ |
|||
import { Type } from 'class-transformer'; |
|||
import { ArrayNotEmpty, IsArray, isNotEmptyObject } from 'class-validator'; |
|||
|
|||
import { UpdateMarketDataDto } from './update-market-data.dto'; |
|||
|
|||
export class UpdateBulkMarketDataDto { |
|||
@ArrayNotEmpty() |
|||
@IsArray() |
|||
@Type(() => UpdateMarketDataDto) |
|||
marketData: UpdateMarketDataDto[]; |
|||
} |
@ -1,6 +1,10 @@ |
|||
import { IsNumber } from 'class-validator'; |
|||
import { IsDate, IsNumber, IsOptional } from 'class-validator'; |
|||
|
|||
export class UpdateMarketDataDto { |
|||
@IsDate() |
|||
@IsOptional() |
|||
date?: Date; |
|||
|
|||
@IsNumber() |
|||
marketPrice: number; |
|||
} |
|||
|
@ -0,0 +1,85 @@ |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; |
|||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config'; |
|||
import { parseSymbol } from '@ghostfolio/common/helper'; |
|||
import { Injectable } from '@nestjs/common'; |
|||
import { SymbolProfile } from '@prisma/client'; |
|||
import got, { Headers } from 'got'; |
|||
|
|||
@Injectable() |
|||
export class OpenFigiDataEnhancerService implements DataEnhancerInterface { |
|||
private static baseUrl = 'https://api.openfigi.com'; |
|||
|
|||
public constructor( |
|||
private readonly configurationService: ConfigurationService |
|||
) {} |
|||
|
|||
public async enhance({ |
|||
response, |
|||
symbol |
|||
}: { |
|||
response: Partial<SymbolProfile>; |
|||
symbol: string; |
|||
}): Promise<Partial<SymbolProfile>> { |
|||
if ( |
|||
!( |
|||
response.assetClass === 'EQUITY' && |
|||
(response.assetSubClass === 'ETF' || response.assetSubClass === 'STOCK') |
|||
) |
|||
) { |
|||
return response; |
|||
} |
|||
|
|||
const headers: Headers = {}; |
|||
const { exchange, ticker } = parseSymbol({ |
|||
symbol, |
|||
dataSource: response.dataSource |
|||
}); |
|||
|
|||
if (this.configurationService.get('OPEN_FIGI_API_KEY')) { |
|||
headers['X-OPENFIGI-APIKEY'] = |
|||
this.configurationService.get('OPEN_FIGI_API_KEY'); |
|||
} |
|||
|
|||
let abortController = new AbortController(); |
|||
|
|||
setTimeout(() => { |
|||
abortController.abort(); |
|||
}, DEFAULT_REQUEST_TIMEOUT); |
|||
|
|||
const mappings = await got |
|||
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, { |
|||
headers, |
|||
json: [{ exchCode: exchange, idType: 'TICKER', idValue: ticker }], |
|||
// @ts-ignore
|
|||
signal: abortController.signal |
|||
}) |
|||
.json<any[]>(); |
|||
|
|||
if (mappings?.length === 1 && mappings[0].data?.length === 1) { |
|||
const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0]; |
|||
|
|||
if (figi) { |
|||
response.figi = figi; |
|||
} |
|||
|
|||
if (compositeFIGI) { |
|||
response.figiComposite = compositeFIGI; |
|||
} |
|||
|
|||
if (shareClassFIGI) { |
|||
response.figiShareClass = shareClassFIGI; |
|||
} |
|||
} |
|||
|
|||
return response; |
|||
} |
|||
|
|||
public getName() { |
|||
return 'OPENFIGI'; |
|||
} |
|||
|
|||
public getTestSymbol() { |
|||
return undefined; |
|||
} |
|||
} |
@ -0,0 +1,67 @@ |
|||
import { readFileSync, readdirSync } from 'fs'; |
|||
import { join } from 'path'; |
|||
|
|||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; |
|||
import { Logger } from '@nestjs/common'; |
|||
import * as cheerio from 'cheerio'; |
|||
|
|||
export class I18nService { |
|||
private localesPath = join(__dirname, 'assets', 'locales'); |
|||
private translations: { [locale: string]: cheerio.CheerioAPI } = {}; |
|||
|
|||
public constructor() { |
|||
this.loadFiles(); |
|||
} |
|||
|
|||
public getTranslation({ |
|||
id, |
|||
languageCode |
|||
}: { |
|||
id: string; |
|||
languageCode: string; |
|||
}): string { |
|||
const $ = this.translations[languageCode]; |
|||
|
|||
if (!$) { |
|||
Logger.warn(`Translation not found for locale '${languageCode}'`); |
|||
} |
|||
|
|||
const translatedText = $( |
|||
`trans-unit[id="${id}"] > ${ |
|||
languageCode === DEFAULT_LANGUAGE_CODE ? 'source' : 'target' |
|||
}` |
|||
).text(); |
|||
|
|||
if (!translatedText) { |
|||
Logger.warn( |
|||
`Translation not found for id '${id}' in locale '${languageCode}'` |
|||
); |
|||
} |
|||
|
|||
return translatedText.trim(); |
|||
} |
|||
|
|||
private loadFiles() { |
|||
try { |
|||
const files = readdirSync(this.localesPath, 'utf-8'); |
|||
|
|||
for (const file of files) { |
|||
const xmlData = readFileSync(join(this.localesPath, file), 'utf8'); |
|||
this.translations[this.parseLanguageCode(file)] = |
|||
this.parseXml(xmlData); |
|||
} |
|||
} catch (error) { |
|||
Logger.error(error, 'I18nService'); |
|||
} |
|||
} |
|||
|
|||
private parseLanguageCode(aFileName: string) { |
|||
const match = aFileName.match(/\.([a-zA-Z]+)\.xlf$/); |
|||
|
|||
return match ? match[1] : DEFAULT_LANGUAGE_CODE; |
|||
} |
|||
|
|||
private parseXml(xmlData: string): cheerio.CheerioAPI { |
|||
return cheerio.load(xmlData, { xmlMode: true }); |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
:host { |
|||
display: block; |
|||
} |
@ -0,0 +1,19 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; |
|||
|
|||
import { I18nPageComponent } from './i18n-page.component'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
canActivate: [AuthGuard], |
|||
component: I18nPageComponent, |
|||
path: '' |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule] |
|||
}) |
|||
export class I18nPageRoutingModule {} |
@ -0,0 +1,21 @@ |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
host: { class: 'page' }, |
|||
selector: 'gf-i18n-page', |
|||
styleUrls: ['./i18n-page.scss'], |
|||
templateUrl: './i18n-page.html' |
|||
}) |
|||
export class I18nPageComponent implements OnInit { |
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor() {} |
|||
|
|||
public ngOnInit() {} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<ul> |
|||
<li i18n="@@metaDescription"> |
|||
Ghostfolio is a personal finance dashboard to keep track of your assets |
|||
like stocks, ETFs or cryptocurrencies across multiple platforms. |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
@ -0,0 +1,12 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
|
|||
import { I18nPageRoutingModule } from './i18n-page-routing.module'; |
|||
import { I18nPageComponent } from './i18n-page.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [I18nPageComponent], |
|||
imports: [CommonModule, I18nPageRoutingModule], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class I18nPageModule {} |
@ -0,0 +1,3 @@ |
|||
:host { |
|||
display: block; |
|||
} |
@ -1,10 +1,3 @@ |
|||
:host { |
|||
display: block; |
|||
|
|||
.fab-container { |
|||
position: fixed; |
|||
right: 2rem; |
|||
bottom: 2rem; |
|||
z-index: 999; |
|||
} |
|||
} |
|||
|
@ -1,6 +1,5 @@ |
|||
export const environment = { |
|||
lastPublish: '{BUILD_TIMESTAMP}', |
|||
production: true, |
|||
stripePublicKey: '', |
|||
version: `v${require('../../../../package.json').version}` |
|||
stripePublicKey: '' |
|||
}; |
|||
|
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue