Browse Source

Merge branch 'main' into feature/move-total-return-from-experimental-to-general-availability

pull/7990/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
a9698e4a2a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      CHANGELOG.md
  2. 4
      Dockerfile
  3. 24
      README.md
  4. 82
      apps/api/src/app/activities/activities.service.spec.ts
  5. 16
      apps/api/src/app/health/health.controller.ts
  6. 365
      apps/api/src/app/import/import.service.spec.ts
  7. 92
      apps/api/src/app/import/import.service.ts
  8. 9
      apps/api/src/main.ts
  9. 12
      apps/api/src/services/prisma/prisma.service.ts
  10. 4
      apps/client/src/app/pages/features/features-page.html
  11. 1
      docker/docker-compose.yml
  12. 2
      libs/common/src/lib/interfaces/index.ts
  13. 3
      libs/common/src/lib/interfaces/responses/liveness-health-response.interface.ts
  14. 4
      libs/ui/src/lib/value/value.component.ts
  15. 392
      package-lock.json
  16. 14
      package.json

17
CHANGELOG.md

@ -7,9 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the _Public API_ with the liveness probe endpoint (`GET api/v1/health/liveness`) (experimental)
- Added the graceful shutdown of the server on `SIGINT` and `SIGTERM`
- Added `stop_grace_period` to the _Ghostfolio_ service in the `docker-compose` file (`docker-compose.yml`)
### Changed ### Changed
- Moved the performance calculation including dividends (total return) from experimental to general availability - Moved the performance calculation including dividends (total return) from experimental to general availability
- Changed the installation of the dependencies from `npm install` to `npm ci` in the `Dockerfile`
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0`
- Upgraded `nestjs` from version `11.2.3` to `11.2.6`
### Fixed
- Fixed the asset profile of the activities after a fee, an interest or a liability with the same symbol in the activities import
### Todo
- Add `stop_grace_period: 1m` to the _Ghostfolio_ service in your `docker-compose` file (see `docker-compose.yml`)
## 3.78.0 - 2026-10-03 ## 3.78.0 - 2026-10-03

4
Dockerfile

@ -20,7 +20,7 @@ COPY ./package.json package.json
COPY ./package-lock.json package-lock.json COPY ./package-lock.json package-lock.json
COPY ./prisma/schema.prisma prisma/ COPY ./prisma/schema.prisma prisma/
RUN npm install RUN npm ci
COPY ./apps apps/ COPY ./apps apps/
COPY ./libs libs/ COPY ./libs libs/
@ -39,7 +39,7 @@ WORKDIR /ghostfolio/dist/apps/api
# package-lock.json needs to be used to ensure the same versions # package-lock.json needs to be used to ensure the same versions
COPY ./package-lock.json /ghostfolio/dist/apps/api/ COPY ./package-lock.json /ghostfolio/dist/apps/api/
RUN npm install RUN npm ci
COPY .config /ghostfolio/dist/apps/api/.config/ COPY .config /ghostfolio/dist/apps/api/.config/
COPY prisma /ghostfolio/dist/apps/api/prisma/ COPY prisma /ghostfolio/dist/apps/api/prisma/

24
README.md

@ -193,6 +193,8 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony
### Health Check (experimental) ### Health Check (experimental)
The endpoint checks the connection to the database and to _Redis_. It is suitable as the readiness probe of an orchestrator (e.g. `readinessProbe` in _Kubernetes_) and as the `healthcheck` in _Docker Compose_. The check of _Redis_ can take up to 5 seconds. Thus, set the timeout of the probe to at least 5 seconds (e.g. `timeoutSeconds: 5` in _Kubernetes_).
#### Request #### Request
`GET http://localhost:3333/api/v1/health` `GET http://localhost:3333/api/v1/health`
@ -211,6 +213,28 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony
} }
``` ```
### Liveness Probe (experimental)
The endpoint answers without a check of the database or _Redis_. It is suitable as the liveness probe of an orchestrator (e.g. `livenessProbe` in _Kubernetes_), so that an outage of a dependency after the start does not restart the container. The database migrations run before the server starts. Thus, also use the endpoint as the startup probe (e.g. `startupProbe` in _Kubernetes_), so that a long migration does not restart the container.
#### Request
`GET http://localhost:3333/api/v1/health/liveness`
**Info:** No Bearer Token is required for liveness probe
#### Response
##### Success
`200 OK`
```
{
"status": "OK"
}
```
### Import Portfolio Data ### Import Portfolio Data
#### Prerequisites #### Prerequisites

82
apps/api/src/app/activities/activities.service.spec.ts

@ -4,6 +4,9 @@ import {
assetProfileDummyData assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service'; import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
INVESTMENT_ACTIVITY_TYPES, INVESTMENT_ACTIVITY_TYPES,
NON_INVESTMENT_ACTIVITY_TYPES NON_INVESTMENT_ACTIVITY_TYPES
@ -41,6 +44,85 @@ describe('ActivitiesService', () => {
); );
}); });
describe('getActivities', () => {
it('returns the activities with the asset profile but without the relation to the symbol profile', async () => {
const assetProfile = {
...assetProfileDummyData,
currency: 'USD',
dataSource: DataSource.YAHOO,
name: 'Apple Inc.',
symbol: 'AAPL'
};
const findMany = jest.fn().mockResolvedValue([
{
...activityDummyData,
account: null,
currency: 'USD',
date: parseDate('2021-01-01'),
fee: 1,
id: 'activity-id',
quantity: 10,
SymbolProfile: {
currency: 'USD',
dataSource: DataSource.YAHOO,
symbol: 'AAPL'
},
tags: [],
type: 'BUY',
unitPrice: 100
}
]);
const service = new ActivitiesService(
null,
null,
null,
null,
null,
null,
null,
{
toCurrencyAtDate: jest.fn().mockResolvedValue(0)
} as unknown as ExchangeRateDataService,
null,
{
order: {
findMany,
count: jest.fn().mockResolvedValue(1)
}
} as unknown as PrismaService,
{
getSymbolProfiles: jest
.fn()
.mockResolvedValue([
{ ...assetProfile, dataSource: DataSource.MANUAL },
assetProfile
])
} as unknown as SymbolProfileService,
null
);
const { activities } = await service.getActivities({
userCurrency: 'USD',
userId: 'user-id'
});
expect(findMany).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.objectContaining({
SymbolProfile: {
select: { currency: true, dataSource: true, symbol: true }
}
})
})
);
expect(activities).toHaveLength(1);
expect(activities[0]).not.toHaveProperty('SymbolProfile');
expect(activities[0].assetProfile).toBe(assetProfile);
});
});
describe('getActivitiesForPortfolioCalculator', () => { describe('getActivitiesForPortfolioCalculator', () => {
it('leaves an activity unchanged when no splits exist', async () => { it('leaves an activity unchanged when no splits exist', async () => {
const activity = createActivity({ symbol: 'AAPL' }); const activity = createActivity({ symbol: 'AAPL' });

16
apps/api/src/app/health/health.controller.ts

@ -3,7 +3,8 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { import {
AiServiceHealthResponse, AiServiceHealthResponse,
DataEnhancerHealthResponse, DataEnhancerHealthResponse,
DataProviderHealthResponse DataProviderHealthResponse,
LivenessHealthResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { import {
@ -33,9 +34,11 @@ export class HealthController {
@Get() @Get()
public async getHealth(@Res() response: Response) { public async getHealth(@Res() response: Response) {
const databaseServiceHealthy = await this.healthService.isDatabaseHealthy(); const [databaseServiceHealthy, redisCacheServiceHealthy] =
const redisCacheServiceHealthy = await Promise.all([
await this.healthService.isRedisCacheHealthy(); this.healthService.isDatabaseHealthy(),
this.healthService.isRedisCacheHealthy()
]);
if (databaseServiceHealthy && redisCacheServiceHealthy) { if (databaseServiceHealthy && redisCacheServiceHealthy) {
return response return response
@ -116,4 +119,9 @@ export class HealthController {
.json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) }); .json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) });
} }
} }
@Get('liveness')
public getLiveness(): LivenessHealthResponse {
return { status: getReasonPhrase(StatusCodes.OK) };
}
} }

365
apps/api/src/app/import/import.service.spec.ts

@ -0,0 +1,365 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { NON_INVESTMENT_ACTIVITY_TYPES } from '@ghostfolio/common/config';
import { CreateOrderDto } from '@ghostfolio/common/dtos';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
Activity,
AssetProfileIdentifier
} from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { DataSource, SymbolProfile } from '@prisma/client';
import { parseISO } from 'date-fns';
import { ImportService } from './import.service';
const CUSTOM_ASSET_PROFILE_SYMBOL = '1ad7d4a2-6b2d-4e0f-9b1f-2c0f8d3e5a7b';
describe('ImportService', () => {
let createActivity: jest.Mock;
let gatherSymbols: jest.Mock;
let importService: ImportService;
beforeEach(() => {
const configuration = {
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: [],
ENABLE_FEATURE_SUBSCRIPTION: false,
MAX_ACTIVITIES_TO_IMPORT: Number.MAX_SAFE_INTEGER
};
createActivity = jest.fn();
gatherSymbols = jest.fn();
const validateActivities = jest.fn(
({ activitiesDto }: { activitiesDto: CreateOrderDto[] }) => {
const assetProfiles: {
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
} = {};
for (const { currency, dataSource, symbol } of activitiesDto) {
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] ??= {
currency,
dataSource,
symbol,
name: symbol
};
}
return assetProfiles;
}
);
importService = new ImportService(
{
getAccounts: jest.fn().mockResolvedValue([])
} as unknown as AccountService,
{
createActivity,
getActivities: jest.fn().mockResolvedValue({ activities: [], count: 0 })
} as unknown as ActivitiesService,
null,
{
get: (key: keyof typeof configuration) => {
return configuration[key];
}
} as unknown as ConfigurationService,
{ gatherSymbols } as unknown as DataGatheringService,
{
validateActivities,
getDataSourceForImport: jest.fn().mockReturnValue(DataSource.MANUAL)
} as unknown as DataProviderService,
{
toCurrencyAtDate: jest.fn().mockResolvedValue(0)
} as unknown as ExchangeRateDataService,
null,
null,
null,
null,
{
getTagsForUser: jest.fn().mockResolvedValue([])
} as unknown as TagService
);
});
describe('import', () => {
it('keeps the asset profile of an investment activity after a non-investment activity', async () => {
mockCreatedAssetProfiles([
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL },
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }
]);
const activities = await importActivities({
activitiesDto: [
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-01T00:00:00.000Z',
symbol: 'AAPL',
type: 'FEE'
}),
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-02T00:00:00.000Z',
symbol: 'AAPL',
type: 'BUY'
})
]
});
expect(
createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create
).toMatchObject({ dataSource: DataSource.YAHOO, symbol: 'AAPL' });
expect(getAssetProfileIdentifiers(activities)).toEqual([
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL },
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }
]);
expect(gatherSymbols.mock.calls[0][0].dataGatheringItems).toEqual([
{
dataSource: DataSource.MANUAL,
date: parseISO('2024-01-01T00:00:00.000Z'),
symbol: CUSTOM_ASSET_PROFILE_SYMBOL
},
{
dataSource: DataSource.YAHOO,
date: parseISO('2024-01-02T00:00:00.000Z'),
symbol: 'AAPL'
}
]);
});
it('shows the custom asset profile of a non-investment activity after an investment activity', async () => {
mockCreatedAssetProfiles([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' },
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
const activities = await importActivities({
activitiesDto: [
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-01T00:00:00.000Z',
symbol: 'AAPL',
type: 'BUY'
}),
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-02T00:00:00.000Z',
symbol: 'AAPL',
type: 'FEE'
})
]
});
expect(
createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create
).toMatchObject({ dataSource: DataSource.YAHOO, symbol: 'AAPL' });
expect(getAssetProfileIdentifiers(activities)).toEqual([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' },
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
});
it.each(NON_INVESTMENT_ACTIVITY_TYPES)(
'refers the next %s activities of a data provider to the custom asset profile which createActivity() has created',
async (type) => {
mockCreatedAssetProfiles([
{
dataSource: DataSource.MANUAL,
symbol: CUSTOM_ASSET_PROFILE_SYMBOL
},
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
await importActivities({
activitiesDto: [
createActivityDto({
type,
dataSource: DataSource.YAHOO,
date: '2024-01-01T00:00:00.000Z',
symbol: 'AAPL'
}),
createActivityDto({
type,
dataSource: DataSource.YAHOO,
date: '2024-01-02T00:00:00.000Z',
symbol: 'AAPL'
})
]
});
expect(
createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create
).toMatchObject({ symbol: CUSTOM_ASSET_PROFILE_SYMBOL });
}
);
it('refers the next activities without a data source to the custom asset profile which createActivity() has created', async () => {
mockCreatedAssetProfiles([
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL },
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
await importActivities({
activitiesDto: [
createActivityDto({
date: '2024-01-01T00:00:00.000Z',
symbol: 'Broker fee',
type: 'FEE'
}),
createActivityDto({
date: '2024-01-02T00:00:00.000Z',
symbol: 'Broker fee',
type: 'FEE'
})
]
});
expect(
createActivity.mock.calls[1][0].SymbolProfile.connectOrCreate.create
).toMatchObject({
dataSource: DataSource.MANUAL,
symbol: CUSTOM_ASSET_PROFILE_SYMBOL
});
});
it('refers the next investment activities without a data source to the custom asset profile which createActivity() has created', async () => {
mockCreatedAssetProfiles([
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL },
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL },
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
await importActivities({
activitiesDto: [
createActivityDto({
date: '2024-01-01T00:00:00.000Z',
symbol: 'Gold',
type: 'BUY'
}),
createActivityDto({
date: '2024-01-02T00:00:00.000Z',
symbol: 'Gold',
type: 'BUY'
}),
createActivityDto({
date: '2024-01-03T00:00:00.000Z',
symbol: 'Gold',
type: 'SELL'
})
]
});
expect(
createActivity.mock.calls.slice(1).map(([activity]) => {
return activity.SymbolProfile.connectOrCreate.create;
})
).toMatchObject([
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL },
{ dataSource: DataSource.MANUAL, symbol: CUSTOM_ASSET_PROFILE_SYMBOL }
]);
expect(gatherSymbols.mock.calls[0][0].dataGatheringItems).toEqual([
{
dataSource: DataSource.MANUAL,
date: parseISO('2024-01-01T00:00:00.000Z'),
symbol: CUSTOM_ASSET_PROFILE_SYMBOL
}
]);
});
it('keeps the asset profiles of the activities in a dry run', async () => {
const activities = await importActivities({
activitiesDto: [
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-01T00:00:00.000Z',
symbol: 'AAPL',
type: 'FEE'
}),
createActivityDto({
dataSource: DataSource.YAHOO,
date: '2024-01-02T00:00:00.000Z',
symbol: 'AAPL',
type: 'BUY'
})
],
isDryRun: true
});
expect(createActivity).not.toHaveBeenCalled();
expect(gatherSymbols).not.toHaveBeenCalled();
expect(getAssetProfileIdentifiers(activities)).toEqual([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' },
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }
]);
});
});
function importActivities({
activitiesDto,
isDryRun
}: {
activitiesDto: CreateOrderDto[];
isDryRun?: boolean;
}) {
return importService.import({
activitiesDto,
isDryRun,
accountsWithBalancesDto: [],
assetProfilesWithMarketDataDto: [],
platformsDto: [],
tagsDto: [],
user: {
id: 'user-id',
permissions: [],
settings: { settings: { baseCurrency: 'USD' } }
} as unknown as UserWithSettings
});
}
function mockCreatedAssetProfiles(assetProfiles: AssetProfileIdentifier[]) {
for (const assetProfile of assetProfiles) {
createActivity.mockImplementationOnce(
({
date,
type
}: Parameters<ActivitiesService['createActivity']>[0]) => {
return { date, type, SymbolProfile: assetProfile };
}
);
}
}
});
function createActivityDto({
dataSource,
date,
symbol,
type
}: Pick<CreateOrderDto, 'dataSource' | 'date' | 'symbol' | 'type'>) {
return {
dataSource,
date,
symbol,
type,
currency: 'USD',
fee: 0,
quantity: 1,
unitPrice: 100
} as CreateOrderDto;
}
function getAssetProfileIdentifiers(activities: Activity[]) {
return activities.map(({ assetProfile: { dataSource, symbol } }) => {
return { dataSource, symbol };
});
}

92
apps/api/src/app/import/import.service.ts

@ -836,6 +836,10 @@ export class ImportService {
const activities: Activity[] = []; const activities: Activity[] = [];
const customAssetProfileSymbols: {
[assetProfileIdentifier: string]: string;
} = {};
for (const activity of activitiesExtendedWithErrors) { for (const activity of activitiesExtendedWithErrors) {
const accountId = activity.accountId; const accountId = activity.accountId;
const comment = activity.comment; const comment = activity.comment;
@ -848,37 +852,15 @@ export class ImportService {
const type = activity.type; const type = activity.type;
const unitPrice = activity.unitPrice; const unitPrice = activity.unitPrice;
const assetProfile = assetProfiles[ const assetProfileIdentifier = getAssetProfileIdentifier({
getAssetProfileIdentifier({ dataSource: activity.assetProfile.dataSource,
dataSource: activity.assetProfile.dataSource, symbol: activity.assetProfile.symbol
symbol: activity.assetProfile.symbol });
})
] ?? { let assetProfile = assetProfiles[assetProfileIdentifier] ?? {
dataSource: activity.assetProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: activity.assetProfile.symbol symbol: activity.assetProfile.symbol
}; };
const {
assetClass,
assetSubClass,
countries,
createdAt,
cusip,
dataSource,
figi,
figiComposite,
figiShareClass,
holdings,
id,
isActive,
isin,
name,
scraperConfiguration,
sectors,
symbol,
symbolMapping,
url,
updatedAt
} = assetProfile;
const validatedAccount = accounts.find(({ id }) => { const validatedAccount = accounts.find(({ id }) => {
return id === accountId; return id === accountId;
}); });
@ -890,7 +872,7 @@ export class ImportService {
let order: let order:
| OrderWithAccount | OrderWithAccount
| (Omit<OrderWithAccount, 'account' | 'tags'> & { | (Omit<OrderWithAccount, 'account' | 'SymbolProfile' | 'tags'> & {
account?: { id: string; name: string }; account?: { id: string; name: string };
tags?: { id: string; name: string }[]; tags?: { id: string; name: string }[];
}); });
@ -916,33 +898,6 @@ export class ImportService {
accountUserId: undefined, accountUserId: undefined,
createdAt: new Date(), createdAt: new Date(),
id: randomUUID(), id: randomUUID(),
SymbolProfile: {
assetClass,
assetSubClass,
countries,
createdAt,
cusip,
dataSource,
figi,
figiComposite,
figiShareClass,
holdings,
id,
isActive,
isin,
name,
scraperConfiguration,
sectors,
symbol,
symbolMapping,
updatedAt,
url,
comment: assetProfile.comment,
currency: assetProfile.currency,
dataGatheringFrequency:
assetProfile.dataGatheringFrequency ?? 'DAILY',
userId: dataSource === 'MANUAL' ? user.id : undefined
},
symbolProfileId: undefined, symbolProfileId: undefined,
tags: previewTags, tags: previewTags,
updatedAt: new Date(), updatedAt: new Date(),
@ -953,7 +908,16 @@ export class ImportService {
continue; continue;
} }
order = await this.activitiesService.createActivity({ const customAssetProfileSymbol =
NON_INVESTMENT_ACTIVITY_TYPES.includes(type) ||
assetProfile.dataSource === DataSource.MANUAL
? customAssetProfileSymbols[assetProfileIdentifier]
: undefined;
const { dataSource, name } = assetProfile;
const symbol = customAssetProfileSymbol ?? assetProfile.symbol;
const createdActivity = await this.activitiesService.createActivity({
comment, comment,
currency, currency,
date, date,
@ -987,10 +951,18 @@ export class ImportService {
userId: user.id userId: user.id
}); });
if (order.SymbolProfile?.symbol) { if (createdActivity.SymbolProfile.dataSource === DataSource.MANUAL) {
// Update symbol that may have been assigned in createOrder() customAssetProfileSymbols[assetProfileIdentifier] =
assetProfile.symbol = order.SymbolProfile.symbol; createdActivity.SymbolProfile.symbol;
assetProfile = {
...assetProfile,
dataSource: DataSource.MANUAL,
symbol: createdActivity.SymbolProfile.symbol
};
} }
order = createdActivity;
} }
const value = new Big(quantity).mul(unitPrice).toNumber(); const value = new Big(quantity).mul(unitPrice).toNumber();

9
apps/api/src/main.ts

@ -14,6 +14,7 @@ import {
import { import {
Logger, Logger,
LogLevel, LogLevel,
ShutdownSignal,
ValidationPipe, ValidationPipe,
VersioningType VersioningType
} from '@nestjs/common'; } from '@nestjs/common';
@ -65,6 +66,14 @@ async function bootstrap() {
}); });
app.enableCors(); app.enableCors();
// Let the open requests and the active queue jobs finish on SIGINT and
// SIGTERM before the process stops. Exit explicitly because Linux ignores
// the re-raised signal if the process runs as PID 1
app.enableShutdownHooks([ShutdownSignal.SIGINT, ShutdownSignal.SIGTERM], {
useProcessExit: true
});
app.enableVersioning({ app.enableVersioning({
defaultVersion: '1', defaultVersion: '1',
type: VersioningType.URI type: VersioningType.URI

12
apps/api/src/services/prisma/prisma.service.ts

@ -2,7 +2,7 @@ import {
Injectable, Injectable,
Logger, Logger,
LogLevel, LogLevel,
OnModuleDestroy, OnApplicationShutdown,
OnModuleInit OnModuleInit
} from '@nestjs/common'; } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
@ -12,7 +12,7 @@ import { Prisma, PrismaClient } from '@prisma/client';
@Injectable() @Injectable()
export class PrismaService export class PrismaService
extends PrismaClient extends PrismaClient
implements OnModuleInit, OnModuleDestroy implements OnApplicationShutdown, OnModuleInit
{ {
private readonly logger = new Logger(PrismaService.name); private readonly logger = new Logger(PrismaService.name);
@ -41,6 +41,10 @@ export class PrismaService
}); });
} }
public async onApplicationShutdown() {
await this.$disconnect();
}
public async onModuleInit() { public async onModuleInit() {
try { try {
await this.$connect(); await this.$connect();
@ -48,8 +52,4 @@ export class PrismaService
this.logger.error(error); this.logger.error(error);
} }
} }
public async onModuleDestroy() {
await this.$disconnect();
}
} }

4
apps/client/src/app/pages/features/features-page.html

@ -224,8 +224,8 @@
> >
</h4> </h4>
<p class="m-0"> <p class="m-0">
Let your AI client read your portfolio and import activities Let your AI client analyze your portfolio and import
into it via the Model Context Protocol (MCP). activities into it via the Model Context Protocol (MCP).
</p> </p>
</div> </div>
</mat-card-content> </mat-card-content>

1
docker/docker-compose.yml

@ -5,6 +5,7 @@ services:
container_name: ghostfolio container_name: ghostfolio
restart: unless-stopped restart: unless-stopped
init: true init: true
stop_grace_period: 1m
cap_drop: cap_drop:
- ALL - ALL
security_opt: security_opt:

2
libs/common/src/lib/interfaces/index.ts

@ -68,6 +68,7 @@ import type { ExportResponse } from './responses/export-response.interface';
import type { HistoricalResponse } from './responses/historical-response.interface'; import type { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
import type { InfoResponse } from './responses/info-response.interface'; import type { InfoResponse } from './responses/info-response.interface';
import type { LivenessHealthResponse } from './responses/liveness-health-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface'; import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface';
@ -156,6 +157,7 @@ export {
InfoResponse, InfoResponse,
InvestmentItem, InvestmentItem,
LineChartItem, LineChartItem,
LivenessHealthResponse,
LookupItem, LookupItem,
LookupResponse, LookupResponse,
MarketData, MarketData,

3
libs/common/src/lib/interfaces/responses/liveness-health-response.interface.ts

@ -0,0 +1,3 @@
export interface LivenessHealthResponse {
status: string;
}

4
libs/ui/src/lib/value/value.component.ts

@ -47,7 +47,7 @@ export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() size: 'large' | 'medium' | 'small' = 'small'; @Input() size: 'large' | 'medium' | 'small' = 'small';
@Input() subLabel?: string = ''; @Input() subLabel?: string = '';
@Input() unit?: string | null = ''; @Input() unit?: string | null = '';
@Input() value?: number | string | null = ''; @Input() value?: Date | number | string | null = '';
@ViewChild('labelContent', { static: false }) @ViewChild('labelContent', { static: false })
labelContent!: ElementRef<HTMLSpanElement>; labelContent!: ElementRef<HTMLSpanElement>;
@ -171,7 +171,7 @@ export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy {
} }
); );
} else { } else {
this.formattedValue = this.value; this.formattedValue = String(this.value);
} }
} }
} }

392
package-lock.json

@ -35,14 +35,14 @@
"@nest-lab/throttler-storage-redis": "1.2.0", "@nest-lab/throttler-storage-redis": "1.2.0",
"@nestjs/bull": "11.0.5", "@nestjs/bull": "11.0.5",
"@nestjs/cache-manager": "3.1.3", "@nestjs/cache-manager": "3.1.3",
"@nestjs/common": "11.2.3", "@nestjs/common": "11.2.6",
"@nestjs/config": "4.0.4", "@nestjs/config": "4.0.4",
"@nestjs/core": "11.2.3", "@nestjs/core": "11.2.6",
"@nestjs/event-emitter": "3.1.0", "@nestjs/event-emitter": "3.1.0",
"@nestjs/jwt": "11.0.2", "@nestjs/jwt": "11.0.2",
"@nestjs/microservices": "11.2.3", "@nestjs/microservices": "11.2.6",
"@nestjs/passport": "11.0.5", "@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.2.3", "@nestjs/platform-express": "11.2.6",
"@nestjs/schedule": "6.1.3", "@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5", "@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0", "@nestjs/throttler": "6.5.0",
@ -50,8 +50,8 @@
"@prisma/adapter-pg": "7.10.0", "@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0", "@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.7", "@rekog/mcp-nest": "2.0.7",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "14.0.0",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "14.0.3",
"ai": "7.0.114", "ai": "7.0.114",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "7.0.1", "big.js": "7.0.1",
@ -124,7 +124,7 @@
"@angular/pwa": "22.1.4", "@angular/pwa": "22.1.4",
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.2.3", "@nestjs/testing": "11.2.6",
"@nx/angular": "23.1.1", "@nx/angular": "23.1.1",
"@nx/eslint-plugin": "23.1.1", "@nx/eslint-plugin": "23.1.1",
"@nx/jest": "23.1.1", "@nx/jest": "23.1.1",
@ -9542,9 +9542,9 @@
} }
}, },
"node_modules/@nestjs/common": { "node_modules/@nestjs/common": {
"version": "11.2.3", "version": "11.2.6",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.2.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.2.6.tgz",
"integrity": "sha512-obdauJXHfthhepbV+LpGe88OeBlR/Kw9lwjLo0Utzc//agoLXYb9DUGhPQWtm81IpBWMv+19eiwcve9MsBZwXA==", "integrity": "sha512-uXs98qEOesTf6rGs0OcvFpRdF4M6JvzyBmoIXcTYvojdte4HiwDYv+whaALoboyfu/fuq2umF74FLQu3V7ZPHw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"file-type": "21.3.4", "file-type": "21.3.4",
@ -9627,9 +9627,9 @@
} }
}, },
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "11.2.3", "version": "11.2.6",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.2.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.2.6.tgz",
"integrity": "sha512-vkA9/Ja0Z3hvqXErSa+HaxrfF+cNXthNFi8VPNEKVli4rMd009yExAl0gLmko/Kf8peDXr72u1RN+j9Da2ukHg==", "integrity": "sha512-KZkwy6KBePPeT3EUj+NACVq6VLT5S+Msbc761NAVVyQ0FyOj7o4I85GEg/cdEPu6KdLqhJFfuqRW2SN+J2ZubQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-safe-stringify": "2.1.1", "fast-safe-stringify": "2.1.1",
@ -9692,9 +9692,9 @@
} }
}, },
"node_modules/@nestjs/microservices": { "node_modules/@nestjs/microservices": {
"version": "11.2.3", "version": "11.2.6",
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.2.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.2.6.tgz",
"integrity": "sha512-HcMm6c8dwKbU0CSOFYxcHZ17YkQawnVzpfT58u24/fAzmsA1WJyfCej5+uONjuG7L/CG76OSxKZlZBNzcxkUiw==", "integrity": "sha512-vtwCIFfVWMBwuSuTfBm/EOQyWTZaVP7GZblDrpLKPggCNkHWwDBLwAd73jyn+DPcDthZQOZLmYESI/3tlaZ8KA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"iterare": "1.2.1", "iterare": "1.2.1",
@ -9760,14 +9760,14 @@
} }
}, },
"node_modules/@nestjs/platform-express": { "node_modules/@nestjs/platform-express": {
"version": "11.2.3", "version": "11.2.6",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.6.tgz",
"integrity": "sha512-YFQvRXT2de1qNL9LJPUBQ31+RsfI4cJ+sbpU9ENM/hDCgoHSEhm7oxUuGGKmhTZBNZEYm8mDYdfoTFmAH1LIJg==", "integrity": "sha512-jPypI/+aueizkYG2Bw2kg8GaNsWE7ekFBSQ/Rwet7wEdBA/+q2FA364UbDabqjlR7C3VSNHmP/JXh+8+jXyDOw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cors": "2.8.6", "cors": "2.8.6",
"express": "5.2.1", "express": "5.2.1",
"multer": "2.2.0", "multer": "2.4.0",
"path-to-regexp": "8.4.2", "path-to-regexp": "8.4.2",
"tslib": "2.8.1" "tslib": "2.8.1"
}, },
@ -10043,9 +10043,9 @@
} }
}, },
"node_modules/@nestjs/testing": { "node_modules/@nestjs/testing": {
"version": "11.2.3", "version": "11.2.6",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.2.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.2.6.tgz",
"integrity": "sha512-7ANDWlkm8Xw4CYIhCNZhtBzANsQUKqjteA2yx/6sjqGyWhekeBKz8wgCJykm0vo+ltrg6U34dZlm2NgiRcNHPQ==", "integrity": "sha512-Oua8EUw3WsnSIwAMIpvyuJxOdUxHeX04fX5MsSIpjaft5CcNnbWhSmM/u/GDDOEcAOrxOfG/KHa7LzzWdS/bxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -13034,129 +13034,200 @@
"tslib": "^2.8.1" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/asn1-asym-key": {
"version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.9.5.tgz",
"integrity": "sha512-U3Q2bUWYNVpfk+oOhPl/+OSHxXaBTQBwMf93jJ7oPi/sbmcY5CnARdjGMwLoXT/ggWSryo5t7A7o3/Gfz0U2WA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-pkcs8": "^2.9.5",
"@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-cms": { "node_modules/@peculiar/asn1-cms": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.5.tgz",
"integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", "integrity": "sha512-4Ivz0fsyFofe0bslNsYcGN9+iETdHX4v+BFwIPr+K/BAt61sHXECP63ff9FntU9zcfVQELJFdUG9so44OjUlBQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.9.5",
"@peculiar/asn1-x509-attr": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-csr": { "node_modules/@peculiar/asn1-csr": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.5.tgz",
"integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", "integrity": "sha512-9dO6PcoJeI+1xJ4rG7eINMa6f9Ix/QdRs5UGZ0D7v/R3TzmsbVr5cqAgfYw4xgS9U54ouHYqEz0bIZBbM/UY1g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-ecc": { "node_modules/@peculiar/asn1-ecc": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.5.tgz",
"integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", "integrity": "sha512-OD+HLlcV7wCL64w0rheX/C0J31DbJce0nf8ulv3UNftQ617JEZE3sQ4cMN6PotXWlJ1M5GQx90G3bywtdKOstw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-pfx": { "node_modules/@peculiar/asn1-pfx": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.5.tgz",
"integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", "integrity": "sha512-btcH7lbcD4410qnjHvv7F0kEuMNKbX8CFAZSs9CDPWHKzT1byLPHYeR40AnUw9z4CzsijMY0daBFYUvVx75sdA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-cms": "^2.9.5",
"@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.9.5",
"@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-rsa": "^2.9.5",
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-pkcs8": { "node_modules/@peculiar/asn1-pkcs8": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.5.tgz",
"integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", "integrity": "sha512-c7UghAGQdBfAHL9JdwcJkAc/lDJKNXfCdYXPXlf8BZdD/fVJjJnXKHxX4gyLhkFTvnGhME7caieSgc2ROtqYTQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-pkcs9": { "node_modules/@peculiar/asn1-pkcs9": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.5.tgz",
"integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", "integrity": "sha512-4R/zMgLSHbQIZRBwP1KzX6iuNHmW3xFWwu9TS9A00N60fmQqp9DkkqnQFyd0mAYMQTrQefuzQ/yP4U8sOLGUgQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-cms": "^2.9.5",
"@peculiar/asn1-pfx": "^2.6.1", "@peculiar/asn1-pfx": "^2.9.5",
"@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.9.5",
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.9.5",
"@peculiar/asn1-x509-attr": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-rsa": { "node_modules/@peculiar/asn1-rsa": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.5.tgz",
"integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", "integrity": "sha512-kHwBLVMkm9ljlM521ovS7y4z4mr8xuHTdNyyoPlDvckBwFKSgs9HqKBryLBeqHqL2YpIgl+Vspl7kzNGG2phzA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-schema": { "node_modules/@peculiar/asn1-schema": {
"version": "2.6.0", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.5.tgz",
"integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", "integrity": "sha512-Ez3wLKVjaxdsLcgeWN4OE31QkM7oBOgKuuBJxldRYAkfYw2C+8zJPcSd/SThnbhszsEOlAmoaS5kIIkN29fGKQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asn1js": "^3.0.6", "@peculiar/utils": "^2.0.2",
"pvtsutils": "^1.3.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-x509": { "node_modules/@peculiar/asn1-x509": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.5.tgz",
"integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", "integrity": "sha512-NP3ecvN6zTDlwCvogIWL7MJN9AZNuskkIbnlhrlkB97Mn2EqirBUQ8GKzsOmdAL3pRvjGu1Uk/a4DNfiZrWxhw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.9.5",
"asn1js": "^3.0.6", "@peculiar/utils": "^2.0.2",
"pvtsutils": "^1.3.6", "asn1js": "^3.0.10",
"tslib": "^2.8.1" "tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
} }
}, },
"node_modules/@peculiar/asn1-x509-attr": { "node_modules/@peculiar/asn1-x509-attr": {
"version": "2.6.1", "version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.5.tgz",
"integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", "integrity": "sha512-PlbL1a74RZiry7puPCZ5hdk/puvivxYJPbjadr2i7DBjHCJFEKCfTRPKL3DILeZFtzJjn95J5EBtSc4/TB9/6Q==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-x509-post-quantum": {
"version": "2.9.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.9.5.tgz",
"integrity": "sha512-CTznDkboSgwRpXSFhta+Pe5cQ12JtojdMrmOZTWYkBSvbW/1R5pflPIHkpSlaILIfGcAaxIVpXq880xCfJYzow==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-asym-key": "^2.9.5",
"@peculiar/asn1-schema": "^2.9.5",
"@peculiar/asn1-x509": "^2.9.5",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.1",
"asn1js": "^3.0.6",
"tslib": "^2.8.1" "tslib": "^2.8.1"
} }
}, },
@ -13164,6 +13235,7 @@
"version": "1.14.3", "version": "1.14.3",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
"integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-cms": "^2.6.0",
@ -13649,67 +13721,6 @@
} }
} }
}, },
"node_modules/@rekog/mcp-nest/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@rekog/mcp-nest/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@rekog/mcp-nest/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@rekog/mcp-nest/node_modules/multer": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz",
"integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@rekog/mcp-nest/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz",
@ -14195,15 +14206,15 @@
} }
}, },
"node_modules/@simplewebauthn/browser": { "node_modules/@simplewebauthn/browser": {
"version": "13.3.0", "version": "14.0.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-14.0.0.tgz",
"integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", "integrity": "sha512-1odWVqeEBTl7lJ9zMKLEsmTlnyrDO5iRcTvfMKKk1WThUnp/i8JJdffdj2icP+tty159s4PgwE3BiMoEW9NFow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@simplewebauthn/server": { "node_modules/@simplewebauthn/server": {
"version": "13.3.1", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.1.tgz", "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-14.0.3.tgz",
"integrity": "sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w==", "integrity": "sha512-D39/0fiDiYmGV+nraY9s63D1Ho2AX/waMLy22/9gUadTUuvbAnstfb4bpbHYbwZuV5ZLDYN1SmzDKuiHzKAA4Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hexagon/base64": "^1.1.27", "@hexagon/base64": "^1.1.27",
@ -14213,7 +14224,31 @@
"@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1",
"@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509": "^2.6.1",
"@peculiar/x509": "^1.14.3" "@peculiar/asn1-x509-post-quantum": "^2.9.4",
"@peculiar/x509": "^2.1.0",
"reflect-metadata": "^0.2.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@simplewebauthn/server/node_modules/@peculiar/x509": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.1.0.tgz",
"integrity": "sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.9.4",
"@peculiar/asn1-csr": "^2.9.4",
"@peculiar/asn1-ecc": "^2.9.4",
"@peculiar/asn1-pkcs9": "^2.9.4",
"@peculiar/asn1-rsa": "^2.9.4",
"@peculiar/asn1-schema": "^2.9.4",
"@peculiar/asn1-x509": "^2.9.4",
"@peculiar/asn1-x509-post-quantum": "^2.9.4",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1",
"tsyringe": "^4.10.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@ -17192,13 +17227,13 @@
} }
}, },
"node_modules/asn1js": { "node_modules/asn1js": {
"version": "3.0.7", "version": "3.0.10",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
"integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"pvtsutils": "^1.3.6", "pvtsutils": "^1.3.6",
"pvutils": "^1.1.3", "pvutils": "^1.1.5",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"engines": { "engines": {
@ -18943,21 +18978,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/confusing-browser-globals": { "node_modules/confusing-browser-globals": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@ -27850,14 +27870,13 @@
} }
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "2.2.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz",
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^1.6.0", "busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18" "type-is": "^1.6.18"
}, },
"engines": { "engines": {
@ -31032,6 +31051,7 @@
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
@ -33481,6 +33501,7 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
@ -34959,12 +34980,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
@ -35276,6 +35291,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/utila": { "node_modules/utila": {

14
package.json

@ -79,14 +79,14 @@
"@nest-lab/throttler-storage-redis": "1.2.0", "@nest-lab/throttler-storage-redis": "1.2.0",
"@nestjs/bull": "11.0.5", "@nestjs/bull": "11.0.5",
"@nestjs/cache-manager": "3.1.3", "@nestjs/cache-manager": "3.1.3",
"@nestjs/common": "11.2.3", "@nestjs/common": "11.2.6",
"@nestjs/config": "4.0.4", "@nestjs/config": "4.0.4",
"@nestjs/core": "11.2.3", "@nestjs/core": "11.2.6",
"@nestjs/event-emitter": "3.1.0", "@nestjs/event-emitter": "3.1.0",
"@nestjs/jwt": "11.0.2", "@nestjs/jwt": "11.0.2",
"@nestjs/microservices": "11.2.3", "@nestjs/microservices": "11.2.6",
"@nestjs/passport": "11.0.5", "@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.2.3", "@nestjs/platform-express": "11.2.6",
"@nestjs/schedule": "6.1.3", "@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5", "@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0", "@nestjs/throttler": "6.5.0",
@ -94,8 +94,8 @@
"@prisma/adapter-pg": "7.10.0", "@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0", "@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.7", "@rekog/mcp-nest": "2.0.7",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "14.0.0",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "14.0.3",
"ai": "7.0.114", "ai": "7.0.114",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "7.0.1", "big.js": "7.0.1",
@ -168,7 +168,7 @@
"@angular/pwa": "22.1.4", "@angular/pwa": "22.1.4",
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.2.3", "@nestjs/testing": "11.2.6",
"@nx/angular": "23.1.1", "@nx/angular": "23.1.1",
"@nx/eslint-plugin": "23.1.1", "@nx/eslint-plugin": "23.1.1",
"@nx/jest": "23.1.1", "@nx/jest": "23.1.1",

Loading…
Cancel
Save