Browse Source

Task/deprecate platform endpoints in favor of platforms endpoints (#8255)

* Deprecate platform endpoints in favor of platforms endpoints

* Update changelog
pull/8257/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
cea7c303c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 28
      apps/api/src/app/__snapshots__/app.module.spec.ts.snap
  3. 2
      apps/api/src/app/app.module.ts
  4. 4
      apps/api/src/app/endpoints/platforms/platform.controller.ts
  5. 113
      apps/api/src/app/endpoints/platforms/platforms.controller.ts
  6. 5
      apps/api/src/app/endpoints/platforms/platforms.module.ts
  7. 2
      apps/api/src/app/import/import.module.ts
  8. 2
      apps/api/src/app/import/import.service.ts
  9. 2
      apps/api/src/app/info/info.module.ts
  10. 2
      apps/api/src/services/platform/platform.module.ts
  11. 7
      apps/api/src/services/platform/platform.service.ts
  12. 20
      apps/client/src/app/components/admin-platform/admin-platform.component.ts
  13. 2
      libs/common/src/lib/interfaces/index.ts
  14. 6
      libs/common/src/lib/interfaces/responses/platform-response.interface.ts
  15. 4
      libs/common/src/lib/interfaces/responses/platforms-response.interface.ts
  16. 29
      libs/ui/src/lib/services/admin.service.ts
  17. 18
      libs/ui/src/lib/services/data.service.ts

1
CHANGELOG.md

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Extracted the file drop area of the import activities dialog to a reusable component
- Improved the performance of the historical market data table of the admin control panel by loading only the latest market price per asset profile
- Introduced a cooldown of 12 hours for the asset profile and exchange rate gathering on activity creation by retaining the completed jobs
- Deprecated the `api/v1/platform` endpoints in favor of the `api/v1/platforms` endpoints
### Fixed

28
apps/api/src/app/__snapshots__/app.module.spec.ts.snap

@ -134,6 +134,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "deletePlatform",
},
"DELETE /platforms/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "deletePlatform",
},
"DELETE /tags/:id": {
"allowDuringImpersonation": true,
"guards": [
@ -557,6 +566,7 @@ exports[`AppModule should keep the security of each route 1`] = `
"permission": "readPlatformsWithAccountCount",
},
"GET /platforms": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
@ -1021,6 +1031,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "createPlatform",
},
"POST /platforms": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "createPlatform",
},
"POST /subscription/redeem-coupon": {
"allowDuringImpersonation": true,
"guards": [
@ -1144,6 +1163,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "updatePlatform",
},
"PUT /platforms/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "updatePlatform",
},
"PUT /portfolio/holding/:dataSource/:symbol/tags": {
"guards": [
"AuthGuard('jwt')",

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

@ -63,7 +63,6 @@ import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
import { LogoModule } from './logo/logo.module';
import { PlatformModule } from './platform/platform.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module';
@ -130,7 +129,6 @@ import { UserModule } from './user/user.module';
LogoModule,
MarketDataModule,
McpModule,
PlatformModule,
PlatformsModule,
PortfolioModule,
PortfolioSnapshotQueueModule,

4
apps/api/src/app/platform/platform.controller.ts → apps/api/src/app/endpoints/platforms/platform.controller.ts

@ -1,6 +1,7 @@
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import { permissions } from '@ghostfolio/common/permissions';
@ -19,8 +20,7 @@ import { AuthGuard } from '@nestjs/passport';
import { Platform } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PlatformService } from './platform.service';
/** @deprecated */
@AllowDuringImpersonation()
@Controller('platform')
export class PlatformController {

113
apps/api/src/app/endpoints/platforms/platforms.controller.ts

@ -1,24 +1,125 @@
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { PlatformsResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import {
PlatformResponse,
PlatformsResponse
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
HttpException,
Inject,
Param,
Post,
Put,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { pick } from 'lodash-es';
@AllowDuringImpersonation()
@Controller('platforms')
export class PlatformsController {
public constructor(private readonly platformService: PlatformService) {}
public constructor(
private readonly platformService: PlatformService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@HasPermission(permissions.createPlatform)
@Post()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async createPlatform(
@Body() data: CreatePlatformDto
): Promise<PlatformResponse> {
const platform = await this.platformService.createPlatform(data);
return pick(platform, ['id', 'name', 'url']);
}
@Delete(':id')
@HasPermission(permissions.deletePlatform)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deletePlatform(@Param('id') id: string): Promise<void> {
const originalPlatform = await this.platformService.getPlatform({
id
});
if (!originalPlatform) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
await this.platformService.deletePlatform({ id });
}
@Get()
@HasPermission(permissions.readPlatforms)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPlatforms(): Promise<PlatformsResponse> {
if (
hasPermission(
this.request.user.permissions,
permissions.readPlatformsWithAccountCount
)
) {
const platformsWithAccountCount =
await this.platformService.getPlatformsWithAccountCount({
orderBy: { name: 'asc' }
});
return { platforms: platformsWithAccountCount };
}
const platforms = await this.platformService.getPlatforms({
orderBy: { name: 'asc' }
});
return { platforms };
return {
platforms: platforms.map((platform) => {
return pick(platform, ['id', 'name', 'url']);
})
};
}
@HasPermission(permissions.updatePlatform)
@Put(':id')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updatePlatform(
@Param('id') id: string,
@Body() data: UpdatePlatformDto
): Promise<PlatformResponse> {
const originalPlatform = await this.platformService.getPlatform({
id
});
if (!originalPlatform) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const platform = await this.platformService.updatePlatform({
data: {
...data
},
where: {
id
}
});
return pick(platform, ['id', 'name', 'url']);
}
}

5
apps/api/src/app/endpoints/platforms/platforms.module.ts

@ -1,11 +1,12 @@
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
import { Module } from '@nestjs/common';
import { PlatformController } from './platform.controller';
import { PlatformsController } from './platforms.controller';
@Module({
controllers: [PlatformsController],
controllers: [PlatformController, PlatformsController],
imports: [PlatformModule]
})
export class PlatformsModule {}

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

@ -1,7 +1,6 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -10,6 +9,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { RedisCacheModule } from '@ghostfolio/api/services/redis-cache/redis-cache.module';

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

@ -1,6 +1,5 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { getTagsWithDraftTag } from '@ghostfolio/api/helper/activity.helper';
import { getMaskedGhostfolioDataSource } from '@ghostfolio/api/helper/data-source.helper';
@ -9,6 +8,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';

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

@ -1,4 +1,3 @@
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -7,6 +6,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { RedisCacheModule } from '@ghostfolio/api/services/redis-cache/redis-cache.module';

2
apps/api/src/app/platform/platform.module.ts → apps/api/src/services/platform/platform.module.ts

@ -2,11 +2,9 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { PlatformController } from './platform.controller';
import { PlatformService } from './platform.service';
@Module({
controllers: [PlatformController],
exports: [PlatformService],
imports: [PrismaModule],
providers: [PlatformService]

7
apps/api/src/app/platform/platform.service.ts → apps/api/src/services/platform/platform.service.ts

@ -49,9 +49,14 @@ export class PlatformService {
});
}
public async getPlatformsWithAccountCount() {
public async getPlatformsWithAccountCount({
orderBy
}: {
orderBy?: Prisma.PlatformOrderByWithRelationInput;
} = {}) {
const platformsWithAccountCount =
await this.prismaService.platform.findMany({
orderBy,
include: {
_count: {
select: { accounts: true }

20
apps/client/src/app/components/admin-platform/admin-platform.component.ts

@ -3,10 +3,11 @@ import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper';
import { PlatformsResponse } from '@ghostfolio/common/interfaces';
import type { Platform } from '@ghostfolio/prisma/browser';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { AdminService, DataService } from '@ghostfolio/ui/services';
import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
import {
@ -62,7 +63,9 @@ import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform-
export class GfAdminPlatformComponent implements OnInit {
public readonly locale = input(getLocale());
protected dataSource = new MatTableDataSource<Platform>();
protected dataSource = new MatTableDataSource<
PlatformsResponse['platforms'][0]
>();
protected readonly displayedColumns = [
'icon',
@ -74,7 +77,7 @@ export class GfAdminPlatformComponent implements OnInit {
protected isLoading = false;
protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected platforms: Platform[];
protected platforms: PlatformsResponse['platforms'];
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
@ -83,7 +86,6 @@ export class GfAdminPlatformComponent implements OnInit {
private readonly paginator = viewChild.required(MatPaginator);
private readonly sort = viewChild.required(MatSort);
private readonly adminService = inject(AdminService);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
@ -139,7 +141,7 @@ export class GfAdminPlatformComponent implements OnInit {
}
private deletePlatform(aId: string) {
this.adminService
this.dataService
.deletePlatform(aId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
@ -157,10 +159,10 @@ export class GfAdminPlatformComponent implements OnInit {
private fetchPlatforms() {
this.isLoading = true;
this.adminService
this.dataService
.fetchPlatforms()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((platforms) => {
.subscribe(({ platforms }) => {
this.platforms = platforms;
this.dataSource = new MatTableDataSource(platforms);
@ -191,7 +193,7 @@ export class GfAdminPlatformComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((platform: CreatePlatformDto | null) => {
if (platform) {
this.adminService
this.dataService
.postPlatform(platform)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
@ -231,7 +233,7 @@ export class GfAdminPlatformComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((platform: UpdatePlatformDto | null) => {
if (platform) {
this.adminService
this.dataService
.putPlatform(platform)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({

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

@ -75,6 +75,7 @@ import type { LivenessHealthResponse } from './responses/liveness-health-respons
import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PlatformResponse } from './responses/platform-response.interface';
import type { PlatformsResponse } from './responses/platforms-response.interface';
import type { PortfolioDividendsResponse } from './responses/portfolio-dividends-response.interface';
import type { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface';
@ -170,6 +171,7 @@ export {
MarketDataOfMarketsResponse,
NullableLineChartItem,
OAuthResponse,
PlatformResponse,
PlatformsResponse,
PortfolioChart,
PortfolioDetails,

6
libs/common/src/lib/interfaces/responses/platform-response.interface.ts

@ -0,0 +1,6 @@
import type { Platform } from '@ghostfolio/prisma/browser';
export interface PlatformResponse extends Pick<
Platform,
'id' | 'name' | 'url'
> {}

4
libs/common/src/lib/interfaces/responses/platforms-response.interface.ts

@ -1,5 +1,5 @@
import type { Platform } from '@ghostfolio/prisma/browser';
import type { PlatformResponse } from './platform-response.interface';
export interface PlatformsResponse {
platforms: Platform[];
platforms: (PlatformResponse & { accountCount?: number })[];
}

29
libs/ui/src/lib/services/admin.service.ts

@ -5,10 +5,8 @@ import {
} from '@ghostfolio/common/config';
import {
CreateAssetProfileSplitDto,
CreatePlatformDto,
MergeAssetProfileDto,
UpdateAssetProfileDto,
UpdatePlatformDto
UpdateAssetProfileDto
} from '@ghostfolio/common/dtos';
import {
AdminData,
@ -21,11 +19,7 @@ import {
EnhancedAssetProfile
} from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import type {
AssetProfileSplit,
MarketData,
Platform
} from '@ghostfolio/prisma/browser';
import type { AssetProfileSplit, MarketData } from '@ghostfolio/prisma/browser';
import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
@ -61,10 +55,6 @@ export class AdminService {
});
}
public deletePlatform(aId: string) {
return this.http.delete<void>(`/api/v1/platform/${aId}`);
}
public deleteAssetProfileSplit({
dataSource,
id,
@ -113,10 +103,6 @@ export class AdminService {
});
}
public fetchPlatforms() {
return this.http.get<Platform[]>('/api/v1/platform');
}
public fetchUserById(id: string) {
return this.http.get<AdminUserResponse>(`/api/v1/admin/user/${id}`);
}
@ -252,17 +238,6 @@ export class AdminService {
);
}
public postPlatform(aPlatform: CreatePlatformDto) {
return this.http.post<Platform>(`/api/v1/platform`, aPlatform);
}
public putPlatform(aPlatform: UpdatePlatformDto) {
return this.http.put<Platform>(
`/api/v1/platform/${aPlatform.id}`,
aPlatform
);
}
public syncDemoUserAccount() {
return this.http.get<void>(`/api/v1/admin/demo-user/sync`);
}

18
libs/ui/src/lib/services/data.service.ts

@ -3,6 +3,7 @@ import {
CreateAccountBalanceDto,
CreateAccountDto,
CreateOrderDto,
CreatePlatformDto,
CreateTagDto,
CreateUserDto,
CreateWatchlistItemDto,
@ -13,6 +14,7 @@ import {
UpdateBulkMarketDataDto,
UpdateOrderDto,
UpdateOwnAccessTokenDto,
UpdatePlatformDto,
UpdatePropertyDto,
UpdateTagDto,
UpdateUserSettingDto
@ -46,6 +48,7 @@ import {
LookupResponse,
MarketDataOfMarketsResponse,
OAuthResponse,
PlatformResponse,
PlatformsResponse,
PortfolioDetails,
PortfolioDividendsResponse,
@ -366,6 +369,10 @@ export class DataService {
return this.http.delete<UserModel>(`/api/v1/user`, { body: aData });
}
public deletePlatform(aId: string) {
return this.http.delete<void>(`/api/v1/platforms/${aId}`);
}
public deleteTag(aId: string) {
return this.http.delete<Tag>(`/api/v1/tags/${aId}`);
}
@ -914,6 +921,10 @@ export class DataService {
return this.http.post<MarketData>(url, marketData);
}
public postPlatform(aPlatform: CreatePlatformDto) {
return this.http.post<PlatformResponse>(`/api/v1/platforms`, aPlatform);
}
public postTag(aTag: CreateTagDto) {
return this.http.post<Tag>(`/api/v1/tags`, aTag);
}
@ -953,6 +964,13 @@ export class DataService {
);
}
public putPlatform(aPlatform: UpdatePlatformDto) {
return this.http.put<PlatformResponse>(
`/api/v1/platforms/${aPlatform.id}`,
aPlatform
);
}
public putTag(aTag: UpdateTagDto) {
return this.http.put<Tag>(`/api/v1/tags/${aTag.id}`, aTag);
}

Loading…
Cancel
Save