Browse Source

Improve symbol validation for assets with manual data source

pull/7467/head
Thomas Kaul 1 month ago
parent
commit
b83d4f2e04
  1. 12
      apps/api/src/app/activities/activities.service.ts
  2. 10
      apps/api/src/app/admin/admin.service.ts
  3. 11
      apps/api/src/services/data-provider/data-provider.service.ts
  4. 6
      libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts
  5. 31
      libs/common/src/lib/helper.spec.ts
  6. 15
      libs/common/src/lib/helper.ts
  7. 18
      libs/common/src/lib/validator-constraints/is-custom-asset-profile-symbol.ts

12
apps/api/src/app/activities/activities.service.ts

@ -20,12 +20,12 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
ghostfolioPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config';
import {
canDeleteAssetProfile,
getAssetProfileIdentifier
getAssetProfileIdentifier,
isValidCustomAssetProfileSymbol
} from '@ghostfolio/common/helper';
import {
ActivitiesResponse,
@ -48,7 +48,6 @@ import {
Type as ActivityType
} from '@prisma/client';
import { Big } from 'big.js';
import { isUUID } from 'class-validator';
import { endOfToday, isAfter } from 'date-fns';
import { groupBy, uniqBy } from 'lodash';
import { randomUUID } from 'node:crypto';
@ -204,10 +203,9 @@ export class ActivitiesService {
let symbol: string;
if (
data.SymbolProfile.connectOrCreate.create.symbol.startsWith(
`${ghostfolioPrefix}_`
) ||
isUUID(data.SymbolProfile.connectOrCreate.create.symbol)
isValidCustomAssetProfileSymbol(
data.SymbolProfile.connectOrCreate.create.symbol
)
) {
// Connect custom asset profile (clone)
symbol = data.SymbolProfile.connectOrCreate.create.symbol;

10
apps/api/src/app/admin/admin.service.ts

@ -7,6 +7,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
ghostfolioPrefix,
PROPERTY_CURRENCIES,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
@ -14,7 +15,8 @@ import {
import {
applyAssetProfileOverrides,
getAssetProfileIdentifier,
getCurrencyFromSymbol
getCurrencyFromSymbol,
hasGhostfolioPrefix
} from '@ghostfolio/common/helper';
import {
AdminData,
@ -63,6 +65,12 @@ export class AdminService {
> {
try {
if (dataSource === 'MANUAL') {
if (!hasGhostfolioPrefix(symbol)) {
throw new BadRequestException(
`symbol "${symbol}" must start with the prefix "${ghostfolioPrefix}_" for the data source "${dataSource}"`
);
}
return this.symbolProfileService.add({
currency,
dataSource,

11
apps/api/src/services/data-provider/data-provider.service.ts

@ -9,6 +9,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import {
DEFAULT_CURRENCY,
DERIVED_CURRENCIES,
ghostfolioPrefix,
PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_DATA_SOURCE_MAPPING
} from '@ghostfolio/common/config';
@ -21,6 +22,7 @@ import {
getStartOfUtcDate,
isCurrency,
isDerivedCurrency,
isValidCustomAssetProfileSymbol,
isValidSearchQuery
} from '@ghostfolio/common/helper';
import {
@ -289,6 +291,15 @@ export class DataProviderService implements OnModuleInit {
continue;
}
if (
dataSource === DataSource.MANUAL &&
!isValidCustomAssetProfileSymbol(symbol)
) {
throw new Error(
`${activityPath}.symbol "${symbol}" must be a UUID or start with the prefix "${ghostfolioPrefix}_" for the data source "${DataSource.MANUAL}"`
);
}
let assetProfile: Partial<SymbolProfile> = { currency };
try {

6
libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts

@ -1,7 +1,8 @@
import { MarketData } from '@ghostfolio/common/interfaces';
import { IsCustomAssetProfileSymbolConstraint } from '@ghostfolio/common/validator-constraints/is-custom-asset-profile-symbol';
import { DataSource } from '@prisma/client';
import { IsArray, IsIn, IsOptional } from 'class-validator';
import { IsArray, IsIn, IsOptional, Validate } from 'class-validator';
import { CreateAssetProfileDto } from './create-asset-profile.dto';
@ -14,4 +15,7 @@ export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto {
@IsArray()
@IsOptional()
marketData?: MarketData[];
@Validate(IsCustomAssetProfileSymbolConstraint)
override symbol: string;
}

31
libs/common/src/lib/helper.spec.ts

@ -10,7 +10,8 @@ import {
isAccountExcluded,
isCurrency,
isCurrencySymbol,
isSplitRatio
isSplitRatio,
isValidCustomAssetProfileSymbol
} from '@ghostfolio/common/helper';
describe('Helper', () => {
@ -326,4 +327,32 @@ describe('Helper', () => {
);
});
});
describe('Is valid custom asset profile symbol', () => {
it('Empty symbol', () => {
expect(isValidCustomAssetProfileSymbol('')).toEqual(false);
});
it('Free-text symbol', () => {
expect(isValidCustomAssetProfileSymbol('Penthouse Apartment')).toEqual(
false
);
});
it('Stock symbol', () => {
expect(isValidCustomAssetProfileSymbol('AAPL')).toEqual(false);
});
it('Symbol with Ghostfolio prefix', () => {
expect(isValidCustomAssetProfileSymbol('GF_PENTHOUSE_APARTMENT')).toEqual(
true
);
});
it('UUID', () => {
expect(
isValidCustomAssetProfileSymbol('7e91b7d4-1430-4212-8380-289a06c9bbc1')
).toEqual(true);
});
});
});

15
libs/common/src/lib/helper.ts

@ -8,7 +8,7 @@ import {
SymbolProfile
} from '@prisma/client';
import { Big } from 'big.js';
import { isISO4217CurrencyCode } from 'class-validator';
import { isISO4217CurrencyCode, isUUID } from 'class-validator';
import {
getDate,
getMonth,
@ -41,6 +41,7 @@ import {
DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks,
ghostfolioPrefix,
SEARCH_QUERY_MINIMUM_LENGTH,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from './config';
@ -466,6 +467,14 @@ export function getYesterday() {
return subDays(new Date(Date.UTC(year, month, day)), 1);
}
export function hasGhostfolioPrefix(aSymbol: string) {
if (!aSymbol) {
return false;
}
return aSymbol.startsWith(`${ghostfolioPrefix}_`);
}
export function interpolate(template: string, context: any) {
return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.');
@ -548,6 +557,10 @@ export function isSplitRatio({
);
}
export function isValidCustomAssetProfileSymbol(aSymbol: string) {
return hasGhostfolioPrefix(aSymbol) || isUUID(aSymbol);
}
export function isValidSearchQuery(aQuery: string) {
return aQuery?.trim().length >= SEARCH_QUERY_MINIMUM_LENGTH;
}

18
libs/common/src/lib/validator-constraints/is-custom-asset-profile-symbol.ts

@ -0,0 +1,18 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
import { ghostfolioPrefix } from '../config';
import { isValidCustomAssetProfileSymbol } from '../helper';
@ValidatorConstraint({ name: 'isCustomAssetProfileSymbol' })
export class IsCustomAssetProfileSymbolConstraint implements ValidatorConstraintInterface {
public defaultMessage() {
return `$property must be a UUID or start with the prefix "${ghostfolioPrefix}_"`;
}
public validate(aSymbol: string) {
return isValidCustomAssetProfileSymbol(aSymbol);
}
}
Loading…
Cancel
Save