Browse Source

Merge branch 'main' into feature/move-language-localization-for-polski-from-experimental-to-general-availability

pull/4200/head
Thomas Kaul 7 months ago
committed by GitHub
parent
commit
1ab502ef27
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 151
      .eslintrc.json
  2. 7
      CHANGELOG.md
  3. 22
      apps/api/.eslintrc.json
  4. 31
      apps/api/eslint.config.cjs
  5. 2
      apps/api/src/app/symbol/symbol.controller.ts
  6. 8
      apps/api/src/services/cryptocurrency/cryptocurrency.service.ts
  7. 312
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  8. 46
      apps/client/.eslintrc.json
  9. 62
      apps/client/eslint.config.cjs
  10. 20
      apps/ui-e2e/.eslintrc.json
  11. 33
      apps/ui-e2e/eslint.config.cjs
  12. 196
      eslint.config.cjs
  13. 21
      libs/common/.eslintrc.json
  14. 28
      libs/common/eslint.config.cjs
  15. 40
      libs/ui/.eslintrc.json
  16. 65
      libs/ui/eslint.config.cjs
  17. 8
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  18. 2
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  19. 1
      nx.json
  20. 1127
      package-lock.json
  21. 22
      package.json

151
.eslintrc.json

@ -1,151 +0,0 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"warn",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
],
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"]
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"]
},
{
"files": ["*.ts"],
"plugins": ["eslint-plugin-import", "@typescript-eslint"],
"extends": [
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": {
"@typescript-eslint/consistent-indexed-object-style": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/member-ordering": "warn",
"@typescript-eslint/naming-convention": [
"off",
{
"selector": "default",
"format": ["camelCase"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
},
{
"selector": ["variable", "classProperty", "typeProperty"],
"format": ["camelCase", "UPPER_CASE"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
},
{
"selector": "objectLiteralProperty",
"format": null
},
{
"selector": "enumMember",
"format": ["camelCase", "UPPER_CASE", "PascalCase"]
},
{
"selector": "typeLike",
"format": ["PascalCase"]
}
],
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-inferrable-types": [
"warn",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-shadow": [
"warn",
{
"hoist": "all"
}
],
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-var-requires": "warn",
"@typescript-eslint/ban-types": "warn",
"arrow-body-style": "off",
"constructor-super": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "warn",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": ["error", "rxjs/Rx"],
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"radix": "error",
"no-unsafe-optional-chaining": "warn",
"no-extra-boolean-cast": "warn",
"no-empty-pattern": "warn",
"no-useless-catch": "warn",
"no-unsafe-finally": "warn",
"no-prototype-builtins": "warn",
"no-async-promise-executor": "warn",
"no-constant-condition": "warn",
// The following rules are part of @typescript-eslint/recommended-type-checked
// and can be remove once solved
"@typescript-eslint/await-thenable": "warn",
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-base-to-string": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-redundant-type-constituents": "warn",
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-enum-comparison": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/unbound-method": "warn",
// The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved
"@typescript-eslint/prefer-nullish-coalescing": "warn" // TODO: Requires strictNullChecks: true
}
}
],
"extends": ["plugin:storybook/recommended"]
}

7
CHANGELOG.md

@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Moved the language localization for Polski (`pl`) from experimental to general availability - Moved the language localization for Polski (`pl`) from experimental to general availability
- Extended the _Financial Modeling Prep_ service
- Switched to _ESLint_’s flat config format
- Upgraded `chart.js` from version `4.2.0` to `4.4.7`
- Upgraded `chartjs-chart-treemap` from version `2.3.1` to `3.1.0`
- Upgraded `chartjs-plugin-annotation` from version `2.1.2` to `3.1.0`
- Upgraded `eslint` dependencies
- Upgraded `uuid` from version `11.0.2` to `11.0.5`
## 2.134.0 - 2025-01-15 ## 2.134.0 - 2025-01-15

22
apps/api/.eslintrc.json

@ -1,22 +0,0 @@
{
"extends": "../../.eslintrc.json",
"ignorePatterns": ["!**/*"],
"rules": {},
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/api/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

31
apps/api/eslint.config.cjs

@ -0,0 +1,31 @@
const baseConfig = require('../../eslint.config.cjs');
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
{
rules: {}
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/api/tsconfig.*?.json']
}
}
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}
}
];

2
apps/api/src/app/symbol/symbol.controller.ts

@ -47,7 +47,7 @@ export class SymbolController {
try { try {
return this.symbolService.lookup({ return this.symbolService.lookup({
includeIndices, includeIndices,
query: query.toLowerCase(), query,
user: this.request.user user: this.request.user
}); });
} catch { } catch {

8
apps/api/src/services/cryptocurrency/cryptocurrency.service.ts

@ -1,3 +1,5 @@
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
const cryptocurrencies = require('../../assets/cryptocurrencies/cryptocurrencies.json'); const cryptocurrencies = require('../../assets/cryptocurrencies/cryptocurrencies.json');
@ -9,7 +11,11 @@ export class CryptocurrencyService {
public isCryptocurrency(aSymbol = '') { public isCryptocurrency(aSymbol = '') {
const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3); const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3);
return this.getCryptocurrencies().includes(cryptocurrencySymbol);
return (
aSymbol.endsWith(DEFAULT_CURRENCY) &&
this.getCryptocurrencies().includes(cryptocurrencySymbol)
);
} }
private getCryptocurrencies() { private getCryptocurrencies() {

312
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -1,4 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { import {
DataProviderInterface, DataProviderInterface,
GetDividendsParams, GetDividendsParams,
@ -10,7 +11,6 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
@ -19,16 +19,31 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import {
import { format, isAfter, isBefore, isSameDay } from 'date-fns'; AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import { isISIN } from 'class-validator';
import { countries } from 'countries-list';
import {
addDays,
format,
isAfter,
isBefore,
isSameDay,
parseISO
} from 'date-fns';
@Injectable() @Injectable()
export class FinancialModelingPrepService implements DataProviderInterface { export class FinancialModelingPrepService implements DataProviderInterface {
private apiKey: string; private apiKey: string;
private readonly URL = 'https://financialmodelingprep.com/api/v3'; private readonly URL = this.getUrl({ version: 3 });
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService
) { ) {
this.apiKey = this.configurationService.get( this.apiKey = this.configurationService.get(
'API_KEY_FINANCIAL_MODELING_PREP' 'API_KEY_FINANCIAL_MODELING_PREP'
@ -44,10 +59,152 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}: { }: {
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
return { const response: Partial<SymbolProfile> = {
symbol, symbol,
dataSource: this.getName() dataSource: this.getName()
}; };
try {
if (this.cryptocurrencyService.isCryptocurrency(symbol)) {
const [quote] = await fetch(
`${this.URL}/quote/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.assetClass = AssetClass.LIQUIDITY;
response.assetSubClass = AssetSubClass.CRYPTOCURRENCY;
response.currency = symbol.substring(symbol.length - 3);
response.name = quote.name;
} else {
const [assetProfile] = await fetch(
`${this.URL}/profile/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
const { assetClass, assetSubClass } =
this.parseAssetClass(assetProfile);
response.assetClass = assetClass;
response.assetSubClass = assetSubClass;
if (assetSubClass === AssetSubClass.ETF) {
const etfCountryWeightings = await fetch(
`${this.URL}/etf-country-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.countries = etfCountryWeightings.map(
({ country: countryName, weightPercentage }) => {
let countryCode: string;
for (const [code, country] of Object.entries(countries)) {
if (country.name === countryName) {
countryCode = code;
break;
}
}
return {
code: countryCode,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
const [portfolioDate] = await fetch(
`${this.getUrl({ version: 4 })}/etf-holdings/portfolio-date?symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
if (portfolioDate) {
const etfHoldings = await fetch(
`${this.getUrl({ version: 4 })}/etf-holdings?date=${portfolioDate.date}&symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
const sortedTopHoldings = etfHoldings
.sort((a, b) => {
return b.pctVal - a.pctVal;
})
.slice(0, 10);
response.holdings = sortedTopHoldings.map(({ name, pctVal }) => {
return { name, weight: pctVal / 100 };
});
}
const etfSectorWeightings = await fetch(
`${this.URL}/etf-sector-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.sectors = etfSectorWeightings.map(
({ sector, weightPercentage }) => {
return {
name: sector,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
} else if (assetSubClass === AssetSubClass.STOCK) {
if (assetProfile.country) {
response.countries = [{ code: assetProfile.country, weight: 1 }];
}
if (assetProfile.sector) {
response.sectors = [{ name: assetProfile.sector, weight: 1 }];
}
}
response.currency = assetProfile.currency;
if (assetProfile.isin) {
response.isin = assetProfile.isin;
}
response.name = assetProfile.companyName;
if (assetProfile.website) {
response.url = assetProfile.website;
}
}
} catch (error) {
let message = error;
if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`;
}
Logger.error(message, 'FinancialModelingPrepService');
}
return response;
} }
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
@ -58,8 +215,54 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}; };
} }
public async getDividends({}: GetDividendsParams) { public async getDividends({
return {}; from,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol,
to
}: GetDividendsParams) {
if (isSameDay(from, to)) {
to = addDays(to, 1);
}
try {
const response: {
[date: string]: IDataProviderHistoricalResponse;
} = {};
const { historical } = await fetch(
`${this.URL}/historical-price-full/stock_dividend/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
historical
.filter(({ date }) => {
return (
(isSameDay(parseISO(date), from) ||
isAfter(parseISO(date), from)) &&
isBefore(parseISO(date), to)
);
})
.forEach(({ adjDividend, date }) => {
response[date] = {
marketPrice: adjDividend
};
});
return response;
} catch (error) {
Logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
'FinancialModelingPrepService'
);
return {};
}
} }
public async getHistorical({ public async getHistorical({
@ -84,14 +287,14 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol]: {} [symbol]: {}
}; };
for (const { close, date } of historical) { for (const { adjClose, date } of historical) {
if ( if (
(isSameDay(parseDate(date), from) || (isSameDay(parseDate(date), from) ||
isAfter(parseDate(date), from)) && isAfter(parseDate(date), from)) &&
isBefore(parseDate(date), to) isBefore(parseDate(date), to)
) { ) {
result[symbol][date] = { result[symbol][date] = {
marketPrice: close marketPrice: adjClose
}; };
} }
} }
@ -130,8 +333,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
).then((res) => res.json()); ).then((res) => res.json());
for (const { price, symbol } of quotes) { for (const { price, symbol } of quotes) {
const { currency } = await this.getAssetProfile({ symbol });
response[symbol] = { response[symbol] = {
currency: DEFAULT_CURRENCY, currency,
dataProviderInfo: this.getDataProviderInfo(), dataProviderInfo: this.getDataProviderInfo(),
dataSource: DataSource.FINANCIAL_MODELING_PREP, dataSource: DataSource.FINANCIAL_MODELING_PREP,
marketPrice: price, marketPrice: price,
@ -161,25 +366,49 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const result = await fetch( if (isISIN(query)) {
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`, const result = await fetch(
{ `${this.getUrl({ version: 4 })}/search/isin?isin=${query}&apikey=${this.apiKey}`,
signal: AbortSignal.timeout( {
this.configurationService.get('REQUEST_TIMEOUT') signal: AbortSignal.timeout(
) this.configurationService.get('REQUEST_TIMEOUT')
} )
).then((res) => res.json()); }
).then((res) => res.json());
items = result.map(({ currency, name, symbol }) => { items = result.map(({ companyName, currency, symbol }) => {
return { return {
// TODO: Add assetClass currency,
// TODO: Add assetSubClass symbol,
currency, assetClass: undefined, // TODO
name, assetSubClass: undefined, // TODO
symbol, dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName() dataSource: this.getName(),
}; name: companyName
}); };
});
} else {
const result = await fetch(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
items = result.map(({ currency, name, symbol }) => {
return {
currency,
name,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName()
};
});
}
} catch (error) { } catch (error) {
let message = error; let message = error;
@ -194,4 +423,29 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return { items }; return { items };
} }
private getUrl({ version }: { version: number }) {
return `https://financialmodelingprep.com/api/v${version}`;
}
private parseAssetClass(profile: any): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
if (profile.isEtf) {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
} else if (profile.isFund) {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.MUTUALFUND;
} else {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
}
return { assetClass, assetSubClass };
}
} }

46
apps/client/.eslintrc.json

@ -1,46 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/client/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts"],
"rules": {
"@angular-eslint/prefer-standalone": "off"
}
}
],
"plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"],
"rules": {
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "gf",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "gf",
"style": "camelCase"
}
]
}
}

62
apps/client/eslint.config.cjs

@ -0,0 +1,62 @@
const baseConfig = require('../../eslint.config.cjs');
const angularEslintPlugin = require('@angular-eslint/eslint-plugin');
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
{
plugins: {
'@angular-eslint': angularEslintPlugin,
'@typescript-eslint': typescriptEslintPlugin
}
},
{
rules: {
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'gf',
style: 'kebab-case'
}
],
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'gf',
style: 'camelCase'
}
]
}
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/client/tsconfig.*?.json']
}
}
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off'
}
}
];

20
apps/ui-e2e/.eslintrc.json

@ -1,20 +0,0 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/ui-e2e/tsconfig.json"]
},
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}

33
apps/ui-e2e/eslint.config.cjs

@ -0,0 +1,33 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../../eslint.config.cjs');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
...compat.extends('plugin:cypress/recommended'),
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/ui-e2e/tsconfig.json']
}
}
},
{
files: ['src/plugins/index.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'no-undef': 'off'
}
}
];

196
eslint.config.cjs

@ -0,0 +1,196 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const storybook = require('eslint-plugin-storybook');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...storybook.configs['flat/recommended'],
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'warn',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*']
}
]
}
],
'@typescript-eslint/no-extra-semi': 'error',
'no-extra-semi': 'off'
}
},
...compat
.config({
extends: ['plugin:@nx/typescript']
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules
}
})),
...compat
.config({
extends: ['plugin:@nx/javascript']
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules
}
})),
...compat
.config({
plugins: ['eslint-plugin-import', '@typescript-eslint'],
extends: [
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:@typescript-eslint/stylistic-type-checked'
]
})
.map((config) => ({
...config,
files: ['**/*.ts'],
rules: {
...config.rules,
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/dot-notation': 'off',
'@typescript-eslint/explicit-member-accessibility': [
'off',
{
accessibility: 'explicit'
}
],
'@typescript-eslint/member-ordering': 'warn',
'@typescript-eslint/naming-convention': [
'off',
{
selector: 'default',
format: ['camelCase'],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: ['variable', 'classProperty', 'typeProperty'],
format: ['camelCase', 'UPPER_CASE'],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'objectLiteralProperty',
format: null
},
{
selector: 'enumMember',
format: ['camelCase', 'UPPER_CASE', 'PascalCase']
},
{
selector: 'typeLike',
format: ['PascalCase']
}
],
'@typescript-eslint/no-empty-interface': 'warn',
'@typescript-eslint/no-inferrable-types': [
'warn',
{
ignoreParameters: true
}
],
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-shadow': [
'warn',
{
hoist: 'all'
}
],
'@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/no-loss-of-precision': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'arrow-body-style': 'off',
'constructor-super': 'error',
eqeqeq: ['error', 'smart'],
'guard-for-in': 'warn',
'id-blacklist': 'off',
'id-match': 'off',
'import/no-deprecated': 'warn',
'no-bitwise': 'error',
'no-caller': 'error',
'no-debugger': 'error',
'no-empty': 'off',
'no-eval': 'error',
'no-fallthrough': 'error',
'no-new-wrappers': 'error',
'no-restricted-imports': ['error', 'rxjs/Rx'],
'no-undef-init': 'error',
'no-underscore-dangle': 'off',
'no-var': 'error',
radix: 'error',
'no-unsafe-optional-chaining': 'warn',
'no-extra-boolean-cast': 'warn',
'no-empty-pattern': 'warn',
'no-useless-catch': 'warn',
'no-unsafe-finally': 'warn',
'no-prototype-builtins': 'warn',
'no-async-promise-executor': 'warn',
'no-constant-condition': 'warn',
// The following rules are part of eslint:recommended
// and can be remove once solved
'no-constant-binary-expression': 'warn',
'no-loss-of-precision': 'warn',
// The following rules are part of @typescript-eslint/recommended-type-checked
// and can be remove once solved
'@typescript-eslint/await-thenable': 'warn',
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-base-to-string': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-misused-promises': 'warn',
'@typescript-eslint/no-redundant-type-constituents': 'warn',
'@typescript-eslint/no-require-imports': 'warn',
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
caughtErrors: 'none'
}
],
'@typescript-eslint/no-wrapper-object-types': 'warn',
'@typescript-eslint/only-throw-error': 'warn',
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
'@typescript-eslint/require-await': 'warn',
'@typescript-eslint/restrict-template-expressions': 'warn',
'@typescript-eslint/unbound-method': 'warn',
// The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved
'@typescript-eslint/prefer-nullish-coalescing': 'warn', // TODO: Requires strictNullChecks: true
'@typescript-eslint/prefer-regexp-exec': 'warn'
}
}))
];

21
libs/common/.eslintrc.json

@ -1,21 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["libs/common/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

28
libs/common/eslint.config.cjs

@ -0,0 +1,28 @@
const baseConfig = require('../../eslint.config.cjs');
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['libs/common/tsconfig.*?.json']
}
}
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}
}
];

40
libs/ui/.eslintrc.json

@ -1,40 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "**/*.stories.ts"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["libs/ui/tsconfig.*?.json"]
},
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "gf",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "gf",
"style": "kebab-case"
}
],
"@angular-eslint/prefer-standalone": "off"
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
}

65
libs/ui/eslint.config.cjs

@ -0,0 +1,65 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../../eslint.config.cjs');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
...compat
.config({
extends: [
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates'
]
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
...config.rules,
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'gf',
style: 'camelCase'
}
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'gf',
style: 'kebab-case'
}
],
'@angular-eslint/prefer-standalone': 'off'
},
languageOptions: {
parserOptions: {
project: ['libs/ui/tsconfig.*?.json']
}
}
})),
...compat
.config({
extends: ['plugin:@nx/angular-template']
})
.map((config) => ({
...config,
files: ['**/*.html'],
rules: {
...config.rules
}
})),
{
ignores: ['**/*.stories.ts']
}
];

8
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -77,7 +77,7 @@ export class GfPortfolioProportionChartComponent
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'pie'>; public chart: Chart<'doughnut'>;
public isLoading = true; public isLoading = true;
private readonly OTHER_KEY = 'OTHER'; private readonly OTHER_KEY = 'OTHER';
@ -257,7 +257,7 @@ export class GfPortfolioProportionChartComponent
}); });
}); });
const datasets: ChartConfiguration['data']['datasets'] = [ const datasets: ChartConfiguration<'doughnut'>['data']['datasets'] = [
{ {
backgroundColor: chartDataSorted.map(([, item]) => { backgroundColor: chartDataSorted.map(([, item]) => {
return item.color; return item.color;
@ -295,7 +295,7 @@ export class GfPortfolioProportionChartComponent
datasets[1].data[1] = Number.MAX_SAFE_INTEGER; datasets[1].data[1] = Number.MAX_SAFE_INTEGER;
} }
const data: ChartConfiguration['data'] = { const data: ChartConfiguration<'doughnut'>['data'] = {
datasets, datasets,
labels labels
}; };
@ -308,7 +308,7 @@ export class GfPortfolioProportionChartComponent
) as unknown; ) as unknown;
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, {
data, data,
options: { options: {
animation: false, animation: false,

2
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -196,7 +196,7 @@ export class GfTreemapChartComponent
min: Math.min(...negativeNetPerformancePercents) min: Math.min(...negativeNetPerformancePercents)
}; };
const data: ChartConfiguration['data'] = { const data: ChartConfiguration<'treemap'>['data'] = {
datasets: [ datasets: [
{ {
backgroundColor: (ctx) => { backgroundColor: (ctx) => {

1
nx.json

@ -63,6 +63,7 @@
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)", "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)",
"!{projectRoot}/.storybook/**/*", "!{projectRoot}/.storybook/**/*",
"!{projectRoot}/eslint.config.cjs",
"!{projectRoot}/jest.config.[jt]s", "!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/src/test-setup.[jt]s", "!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/tsconfig.storybook.json", "!{projectRoot}/tsconfig.storybook.json",

1127
package-lock.json

File diff suppressed because it is too large

22
package.json

@ -96,10 +96,10 @@
"bull": "4.16.2", "bull": "4.16.2",
"cache-manager": "5.7.6", "cache-manager": "5.7.6",
"cache-manager-redis-yet": "5.1.4", "cache-manager-redis-yet": "5.1.4",
"chart.js": "4.2.0", "chart.js": "4.4.7",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-treemap": "2.3.1", "chartjs-chart-treemap": "3.1.0",
"chartjs-plugin-annotation": "2.1.2", "chartjs-plugin-annotation": "3.1.0",
"chartjs-plugin-datalabels": "2.2.0", "chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.0.0", "cheerio": "1.0.0",
"class-transformer": "0.5.1", "class-transformer": "0.5.1",
@ -134,7 +134,7 @@
"stripe": "17.3.0", "stripe": "17.3.0",
"svgmap": "2.6.0", "svgmap": "2.6.0",
"twitter-api-v2": "1.14.2", "twitter-api-v2": "1.14.2",
"uuid": "11.0.2", "uuid": "11.0.5",
"yahoo-finance2": "2.11.3", "yahoo-finance2": "2.11.3",
"zone.js": "0.15.0" "zone.js": "0.15.0"
}, },
@ -150,6 +150,8 @@
"@angular/language-service": "19.0.5", "@angular/language-service": "19.0.5",
"@angular/localize": "19.0.5", "@angular/localize": "19.0.5",
"@angular/pwa": "19.0.6", "@angular/pwa": "19.0.6",
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.18.0",
"@nestjs/schematics": "10.0.1", "@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3", "@nestjs/testing": "10.1.3",
"@nx/angular": "20.3.0", "@nx/angular": "20.3.0",
@ -179,15 +181,15 @@
"@types/node": "20.14.10", "@types/node": "20.14.10",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16", "@types/passport-google-oauth20": "2.0.16",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "6.21.0", "@typescript-eslint/parser": "8.20.0",
"codelyzer": "6.0.1", "codelyzer": "6.0.1",
"cypress": "6.2.1", "cypress": "6.2.1",
"eslint": "8.57.0", "eslint": "9.18.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-cypress": "2.15.1", "eslint-plugin-cypress": "3.2.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.31.0",
"eslint-plugin-storybook": "0.6.15", "eslint-plugin-storybook": "0.10.2",
"husky": "9.1.7", "husky": "9.1.7",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",

Loading…
Cancel
Save