Browse Source

Initial implementation

pull/187/head
Thomas 4 years ago
parent
commit
6355fdb3ba
  1. 1
      apps/api/src/app/experimental/experimental.service.ts
  2. 8
      apps/api/src/app/order/order.controller.ts
  3. 34
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 6
      apps/api/src/models/order.ts
  5. 129
      apps/api/src/models/portfolio.ts
  6. 10
      apps/api/src/services/data-gathering.service.ts
  7. 1
      apps/api/src/services/interfaces/interfaces.ts
  8. 26
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  9. 11
      apps/client/src/app/components/transactions-table/transactions-table.component.html
  10. 2
      apps/client/src/styles/bootstrap.scss
  11. 2
      prisma/migrations/20210627204133_added_is_draft_to_orders/migration.sql
  12. 1
      prisma/schema.prisma

1
apps/api/src/app/experimental/experimental.service.ts

@ -43,6 +43,7 @@ export class ExperimentalService {
date: parseISO(order.date), date: parseISO(order.date),
fee: 0, fee: 0,
id: undefined, id: undefined,
isDraft: false,
platformId: undefined, platformId: undefined,
symbolProfileId: undefined, symbolProfileId: undefined,
type: Type.BUY, type: Type.BUY,

8
apps/api/src/app/order/order.controller.ts

@ -22,7 +22,7 @@ import {
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client'; import { Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns'; import { endOfToday, isAfter, parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateOrderDto } from './create-order.dto'; import { CreateOrderDto } from './create-order.dto';
@ -129,6 +129,8 @@ export class OrderController {
const accountId = data.accountId; const accountId = data.accountId;
delete data.accountId; delete data.accountId;
const isDraft = isAfter(date, endOfToday());
return this.orderService.createOrder( return this.orderService.createOrder(
{ {
...data, ...data,
@ -138,6 +140,7 @@ export class OrderController {
} }
}, },
date, date,
isDraft,
SymbolProfile: { SymbolProfile: {
connectOrCreate: { connectOrCreate: {
where: { where: {
@ -192,11 +195,14 @@ export class OrderController {
const accountId = data.accountId; const accountId = data.accountId;
delete data.accountId; delete data.accountId;
const isDraft = isAfter(date, endOfToday());
return this.orderService.updateOrder( return this.orderService.updateOrder(
{ {
data: { data: {
...data, ...data,
date, date,
isDraft,
Account: { Account: {
connect: { connect: {
id_userId: { id: accountId, userId: this.request.user.id } id_userId: { id: accountId, userId: this.request.user.id }

34
apps/api/src/app/portfolio/portfolio.service.ts

@ -14,11 +14,13 @@ import { REQUEST } from '@nestjs/core';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { import {
add, add,
endOfToday,
format, format,
getDate, getDate,
getMonth, getMonth,
getYear, getYear,
isAfter, isAfter,
isBefore,
isSameDay, isSameDay,
parse, parse,
parseISO, parseISO,
@ -26,6 +28,7 @@ import {
setMonth, setMonth,
sub sub
} from 'date-fns'; } from 'date-fns';
import { port } from 'envalid';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import * as roundTo from 'round-to'; import * as roundTo from 'round-to';
@ -52,7 +55,7 @@ export class PortfolioService {
public async createPortfolio(aUserId: string): Promise<Portfolio> { public async createPortfolio(aUserId: string): Promise<Portfolio> {
let portfolio: Portfolio; let portfolio: Portfolio;
let stringifiedPortfolio = await this.redisCacheService.get( const stringifiedPortfolio = await this.redisCacheService.get(
`${aUserId}.portfolio` `${aUserId}.portfolio`
); );
@ -63,9 +66,8 @@ export class PortfolioService {
const { const {
orders, orders,
portfolioItems portfolioItems
}: { orders: IOrder[]; portfolioItems: PortfolioItem[] } = JSON.parse( }: { orders: IOrder[]; portfolioItems: PortfolioItem[] } =
stringifiedPortfolio JSON.parse(stringifiedPortfolio);
);
portfolio = new Portfolio( portfolio = new Portfolio(
this.dataProviderService, this.dataProviderService,
@ -104,12 +106,18 @@ export class PortfolioService {
} }
// Enrich portfolio with current data // Enrich portfolio with current data
return await portfolio.addCurrentPortfolioItems(); await portfolio.addCurrentPortfolioItems();
// Enrich portfolio with future data
await portfolio.addFuturePortfolioItems();
return portfolio;
} }
public async findAll(aImpersonationId: string): Promise<PortfolioItem[]> { public async findAll(aImpersonationId: string): Promise<PortfolioItem[]> {
try { try {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId, aImpersonationId,
this.request.user.id this.request.user.id
); );
@ -127,7 +135,8 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aDateRange: DateRange = 'max' aDateRange: DateRange = 'max'
): Promise<HistoricalDataItem[]> { ): Promise<HistoricalDataItem[]> {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId, aImpersonationId,
this.request.user.id this.request.user.id
); );
@ -148,6 +157,11 @@ export class PortfolioService {
return portfolio return portfolio
.get() .get()
.filter((portfolioItem) => { .filter((portfolioItem) => {
if (isAfter(parseISO(portfolioItem.date), endOfToday())) {
// Filter out future dates
return false;
}
if (dateRangeDate === undefined) { if (dateRangeDate === undefined) {
return true; return true;
} }
@ -170,7 +184,8 @@ export class PortfolioService {
public async getOverview( public async getOverview(
aImpersonationId: string aImpersonationId: string
): Promise<PortfolioOverview> { ): Promise<PortfolioOverview> {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId, aImpersonationId,
this.request.user.id this.request.user.id
); );
@ -195,7 +210,8 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aSymbol: string aSymbol: string
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId, aImpersonationId,
this.request.user.id this.request.user.id
); );

6
apps/api/src/models/order.ts

@ -10,6 +10,7 @@ export class Order {
private fee: number; private fee: number;
private date: string; private date: string;
private id: string; private id: string;
private isDraft: boolean;
private quantity: number; private quantity: number;
private symbol: string; private symbol: string;
private symbolProfile: SymbolProfile; private symbolProfile: SymbolProfile;
@ -23,6 +24,7 @@ export class Order {
this.fee = data.fee; this.fee = data.fee;
this.date = data.date; this.date = data.date;
this.id = data.id || uuidv4(); this.id = data.id || uuidv4();
this.isDraft = data.isDraft ?? false;
this.quantity = data.quantity; this.quantity = data.quantity;
this.symbol = data.symbol; this.symbol = data.symbol;
this.symbolProfile = data.symbolProfile; this.symbolProfile = data.symbolProfile;
@ -52,6 +54,10 @@ export class Order {
return this.id; return this.id;
} }
public getIsDraft() {
return this.isDraft;
}
public getQuantity() { public getQuantity() {
return this.quantity; return this.quantity;
} }

129
apps/api/src/models/portfolio.ts

@ -73,7 +73,7 @@ export class Portfolio implements PortfolioInterface {
const [portfolioItemsYesterday] = this.get(yesterday); const [portfolioItemsYesterday] = this.get(yesterday);
let positions: { [symbol: string]: Position } = {}; const positions: { [symbol: string]: Position } = {};
this.getSymbols().forEach((symbol) => { this.getSymbols().forEach((symbol) => {
positions[symbol] = { positions[symbol] = {
@ -105,10 +105,45 @@ export class Portfolio implements PortfolioInterface {
); );
// Set value after pushing today's portfolio items // Set value after pushing today's portfolio items
this.portfolioItems[portfolioItemsLength - 1].value = this.getValue( this.portfolioItems[portfolioItemsLength - 1].value =
today this.getValue(today);
}
return this;
}
public async addFuturePortfolioItems() {
let investment = this.getInvestment(new Date());
this.getOrders()
.filter((order) => order.getIsDraft() === true)
.forEach((order) => {
const portfolioItem = this.portfolioItems.find((item) => {
return item.date === order.getDate();
});
if (portfolioItem) {
portfolioItem.investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
); );
} else {
investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems.push({
investment,
date: order.getDate(),
grossPerformancePercent: 0,
positions: {},
value: 0
});
} }
});
return this; return this;
} }
@ -129,6 +164,7 @@ export class Portfolio implements PortfolioInterface {
fee, fee,
date, date,
id, id,
isDraft,
quantity, quantity,
symbol, symbol,
symbolProfile, symbolProfile,
@ -142,6 +178,7 @@ export class Portfolio implements PortfolioInterface {
fee, fee,
date, date,
id, id,
isDraft,
quantity, quantity,
symbol, symbol,
symbolProfile, symbolProfile,
@ -178,9 +215,12 @@ export class Portfolio implements PortfolioInterface {
if (filteredPortfolio) { if (filteredPortfolio) {
return [cloneDeep(filteredPortfolio)]; return [cloneDeep(filteredPortfolio)];
} }
return [];
} }
return cloneDeep(this.portfolioItems); return cloneDeep(this.portfolioItems);
// return [];
} }
public getCommittedFunds() { public getCommittedFunds() {
@ -239,12 +279,10 @@ export class Portfolio implements PortfolioInterface {
if ( if (
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY]?.current accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY]?.current
) { ) {
accounts[ accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].current +=
orderOfSymbol.getAccount()?.name || UNKNOWN_KEY currentValueOfSymbol;
].current += currentValueOfSymbol; accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].original +=
accounts[ originalValueOfSymbol;
orderOfSymbol.getAccount()?.name || UNKNOWN_KEY
].original += originalValueOfSymbol;
} else { } else {
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY] = { accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY] = {
current: currentValueOfSymbol, current: currentValueOfSymbol,
@ -365,7 +403,11 @@ export class Portfolio implements PortfolioInterface {
} }
public getMinDate() { public getMinDate() {
if (this.orders.length > 0) { const orders = this.getOrders().filter(
(order) => order.getIsDraft() === false
);
if (orders.length > 0) {
return new Date(this.orders[0].getDate()); return new Date(this.orders[0].getDate());
} }
@ -492,7 +534,9 @@ export class Portfolio implements PortfolioInterface {
} }
} }
} else { } else {
symbols = this.orders.map((order) => { symbols = this.orders
.filter((order) => order.getIsDraft() === false)
.map((order) => {
return order.getSymbol(); return order.getSymbol();
}); });
} }
@ -503,7 +547,9 @@ export class Portfolio implements PortfolioInterface {
public getTotalBuy() { public getTotalBuy() {
return this.orders return this.orders
.filter((order) => order.getType() === 'BUY') .filter(
(order) => order.getIsDraft() === false && order.getType() === 'BUY'
)
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.getTotal(), order.getTotal(),
@ -516,7 +562,9 @@ export class Portfolio implements PortfolioInterface {
public getTotalSell() { public getTotalSell() {
return this.orders return this.orders
.filter((order) => order.getType() === 'SELL') .filter(
(order) => order.getIsDraft() === false && order.getType() === 'SELL'
)
.map((order) => { .map((order) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
order.getTotal(), order.getTotal(),
@ -583,6 +631,7 @@ export class Portfolio implements PortfolioInterface {
currency: order.currency, currency: order.currency,
date: order.date.toISOString(), date: order.date.toISOString(),
fee: order.fee, fee: order.fee,
isDraft: order.isDraft,
quantity: order.quantity, quantity: order.quantity,
symbol: order.symbol, symbol: order.symbol,
symbolProfile: order.SymbolProfile, symbolProfile: order.SymbolProfile,
@ -686,10 +735,10 @@ export class Portfolio implements PortfolioInterface {
this.portfolioItems.push( this.portfolioItems.push(
cloneDeep({ cloneDeep({
positions,
date: yesterday.toISOString(), date: yesterday.toISOString(),
grossPerformancePercent: 0, grossPerformancePercent: 0,
investment: 0, investment: 0,
positions: positions,
value: 0 value: 0
}) })
); );
@ -746,8 +795,6 @@ export class Portfolio implements PortfolioInterface {
} }
private updatePortfolioItems() { private updatePortfolioItems() {
// console.time('update-portfolio-items');
let currentDate = new Date(); let currentDate = new Date();
const year = getYear(currentDate); const year = getYear(currentDate);
@ -771,6 +818,7 @@ export class Portfolio implements PortfolioInterface {
} }
this.orders.forEach((order) => { this.orders.forEach((order) => {
if (order.getIsDraft() === false) {
let index = this.portfolioItems.findIndex((item) => { let index = this.portfolioItems.findIndex((item) => {
const dateOfOrder = setDate(parseISO(order.getDate()), 1); const dateOfOrder = setDate(parseISO(order.getDate()), 1);
return isSameDay(parseISO(item.date), dateOfOrder); return isSameDay(parseISO(item.date), dateOfOrder);
@ -783,9 +831,8 @@ export class Portfolio implements PortfolioInterface {
for (let i = index; i < this.portfolioItems.length; i++) { for (let i = index; i < this.portfolioItems.length; i++) {
// Set currency // Set currency
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[order.getSymbol()].currency =
order.getSymbol() order.getCurrency();
].currency = order.getCurrency();
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[
order.getSymbol() order.getSymbol()
@ -795,19 +842,14 @@ export class Portfolio implements PortfolioInterface {
if ( if (
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate !this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
) { ) {
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate =
order.getSymbol() resetHours(parseISO(order.getDate())).toISOString();
].firstBuyDate = resetHours(
parseISO(order.getDate())
).toISOString();
} }
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[order.getSymbol()].quantity +=
order.getSymbol() order.getQuantity();
].quantity += order.getQuantity(); this.portfolioItems[i].positions[order.getSymbol()].investment +=
this.portfolioItems[i].positions[ this.exchangeRateDataService.toCurrency(
order.getSymbol()
].investment += this.exchangeRateDataService.toCurrency(
order.getTotal(), order.getTotal(),
order.getCurrency(), order.getCurrency(),
this.user.Settings.currency this.user.Settings.currency
@ -816,29 +858,28 @@ export class Portfolio implements PortfolioInterface {
order.getSymbol() order.getSymbol()
].investmentInOriginalCurrency += order.getTotal(); ].investmentInOriginalCurrency += order.getTotal();
this.portfolioItems[ this.portfolioItems[i].investment +=
i this.exchangeRateDataService.toCurrency(
].investment += this.exchangeRateDataService.toCurrency(
order.getTotal(), order.getTotal(),
order.getCurrency(), order.getCurrency(),
this.user.Settings.currency this.user.Settings.currency
); );
} else if (order.getType() === 'SELL') { } else if (order.getType() === 'SELL') {
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[order.getSymbol()].quantity -=
order.getSymbol() order.getQuantity();
].quantity -= order.getQuantity();
if ( if (
this.portfolioItems[i].positions[order.getSymbol()].quantity === 0 this.portfolioItems[i].positions[order.getSymbol()].quantity === 0
) { ) {
this.portfolioItems[i].positions[order.getSymbol()].investment = 0;
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[
order.getSymbol() order.getSymbol()
].investmentInOriginalCurrency = 0; ].investment = 0;
} else {
this.portfolioItems[i].positions[ this.portfolioItems[i].positions[
order.getSymbol() order.getSymbol()
].investment -= this.exchangeRateDataService.toCurrency( ].investmentInOriginalCurrency = 0;
} else {
this.portfolioItems[i].positions[order.getSymbol()].investment -=
this.exchangeRateDataService.toCurrency(
order.getTotal(), order.getTotal(),
order.getCurrency(), order.getCurrency(),
this.user.Settings.currency this.user.Settings.currency
@ -848,9 +889,8 @@ export class Portfolio implements PortfolioInterface {
].investmentInOriginalCurrency -= order.getTotal(); ].investmentInOriginalCurrency -= order.getTotal();
} }
this.portfolioItems[ this.portfolioItems[i].investment -=
i this.exchangeRateDataService.toCurrency(
].investment -= this.exchangeRateDataService.toCurrency(
order.getTotal(), order.getTotal(),
order.getCurrency(), order.getCurrency(),
this.user.Settings.currency this.user.Settings.currency
@ -870,8 +910,7 @@ export class Portfolio implements PortfolioInterface {
currentValue / this.portfolioItems[i].investment - 1 || 0; currentValue / this.portfolioItems[i].investment - 1 || 0;
this.portfolioItems[i].value = currentValue; this.portfolioItems[i].value = currentValue;
} }
}
}); });
// console.timeEnd('update-portfolio-items');
} }
} }

10
apps/api/src/services/data-gathering.service.ts

@ -224,7 +224,10 @@ export class DataGatheringService {
const distinctOrders = await this.prisma.order.findMany({ const distinctOrders = await this.prisma.order.findMany({
distinct: ['symbol'], distinct: ['symbol'],
orderBy: [{ symbol: 'asc' }], orderBy: [{ symbol: 'asc' }],
select: { dataSource: true, symbol: true } select: { dataSource: true, symbol: true },
where: {
isDraft: false
}
}); });
const distinctOrdersWithDate: IDataGatheringItem[] = distinctOrders const distinctOrdersWithDate: IDataGatheringItem[] = distinctOrders
@ -280,7 +283,10 @@ export class DataGatheringService {
const distinctOrders = await this.prisma.order.findMany({ const distinctOrders = await this.prisma.order.findMany({
distinct: ['symbol'], distinct: ['symbol'],
orderBy: [{ date: 'asc' }], orderBy: [{ date: 'asc' }],
select: { dataSource: true, date: true, symbol: true } select: { dataSource: true, date: true, symbol: true },
where: {
isDraft: false
}
}); });
return [ return [

1
apps/api/src/services/interfaces/interfaces.ts

@ -22,6 +22,7 @@ export interface IOrder {
date: string; date: string;
fee: number; fee: number;
id?: string; id?: string;
isDraft: boolean;
quantity: number; quantity: number;
symbol: string; symbol: string;
symbolProfile: SymbolProfile; symbolProfile: SymbolProfile;

26
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -19,6 +19,7 @@ import {
TimeScale TimeScale
} from 'chart.js'; } from 'chart.js';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import { addMonths, parseISO, subMonths } from 'date-fns';
@Component({ @Component({
selector: 'gf-investment-chart', selector: 'gf-investment-chart',
@ -52,9 +53,30 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
} }
} }
public ngOnDestroy() {
this.chart?.destroy();
}
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
if (this.portfolioItems?.length > 0) {
// Extend chart by three months (before)
const firstItem = this.portfolioItems[0];
this.portfolioItems.unshift({
...firstItem,
date: subMonths(parseISO(firstItem.date), 3).toISOString(),
investment: 0
});
// Extend chart by three months (after)
const lastItem = this.portfolioItems[this.portfolioItems.length - 1];
this.portfolioItems.push({
...lastItem,
date: addMonths(parseISO(lastItem.date), 3).toISOString()
});
}
const data = { const data = {
labels: this.portfolioItems.map((position) => { labels: this.portfolioItems.map((position) => {
return position.date; return position.date;
@ -122,8 +144,4 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
} }
} }
} }
public ngOnDestroy() {
this.chart?.destroy();
}
} }

11
apps/client/src/app/components/transactions-table/transactions-table.component.html

@ -100,24 +100,27 @@
Symbol Symbol
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center">
{{ element.symbol | gfSymbol }} {{ element.symbol | gfSymbol }}
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
>Draft</span
>
</div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="currency"> <ng-container matColumnDef="currency">
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell justify-content-center px-1" class="d-none d-lg-table-cell px-1"
mat-header-cell
i18n i18n
mat-header-cell
mat-sort-header mat-sort-header
> >
Currency Currency
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-center">
{{ element.currency }} {{ element.currency }}
</div>
</td> </td>
</ng-container> </ng-container>

2
apps/client/src/styles/bootstrap.scss

@ -27,7 +27,7 @@
// @import '~bootstrap/scss/card'; // @import '~bootstrap/scss/card';
// @import '~bootstrap/scss/breadcrumb'; // @import '~bootstrap/scss/breadcrumb';
// @import '~bootstrap/scss/pagination'; // @import '~bootstrap/scss/pagination';
// @import '~bootstrap/scss/badge'; @import '~bootstrap/scss/badge';
// @import '~bootstrap/scss/jumbotron'; // @import '~bootstrap/scss/jumbotron';
// @import '~bootstrap/scss/alert'; // @import '~bootstrap/scss/alert';
// @import '~bootstrap/scss/progress'; // @import '~bootstrap/scss/progress';

2
prisma/migrations/20210627204133_added_is_draft_to_orders/migration.sql

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;

1
prisma/schema.prisma

@ -79,6 +79,7 @@ model Order {
date DateTime date DateTime
fee Float fee Float
id String @default(uuid()) id String @default(uuid())
isDraft Boolean @default(false)
quantity Float quantity Float
symbol String symbol String
SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id]) SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])

Loading…
Cancel
Save