chore(trading): handle positions with market data gql errors (#2884)
This commit is contained in:
parent
2e8dd294de
commit
4dd63da62b
@ -1,3 +1,6 @@
|
|||||||
|
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||||
|
import { marketsDataQuery } from '@vegaprotocol/mock';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.mockTradingPage();
|
cy.mockTradingPage();
|
||||||
cy.mockSubscription();
|
cy.mockSubscription();
|
||||||
@ -17,6 +20,47 @@ describe('positions', { tags: '@smoke' }, () => {
|
|||||||
validatePositionsDisplayed();
|
validatePositionsDisplayed();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders position among some graphql errors', () => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
message: 'no market data for market: market-2',
|
||||||
|
path: ['market', 'data'],
|
||||||
|
extensions: {
|
||||||
|
code: 13,
|
||||||
|
type: 'Internal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const marketData = marketsDataQuery();
|
||||||
|
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||||
|
const replace =
|
||||||
|
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||||
|
return { ...market, node: { ...market.node, data: replace } };
|
||||||
|
});
|
||||||
|
const overrides = {
|
||||||
|
...marketData,
|
||||||
|
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||||
|
};
|
||||||
|
cy.mockGQL((req) => {
|
||||||
|
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||||
|
});
|
||||||
|
cy.visit('/#/markets/market-0');
|
||||||
|
const emptyCells = [
|
||||||
|
'notional',
|
||||||
|
'markPrice',
|
||||||
|
'liquidationPrice',
|
||||||
|
'currentLeverage',
|
||||||
|
'averageEntryPrice',
|
||||||
|
];
|
||||||
|
cy.getByTestId('tab-positions').within(() => {
|
||||||
|
cy.get('[row-id="market-2"]').within(() => {
|
||||||
|
emptyCells.forEach((cell) => {
|
||||||
|
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function validatePositionsDisplayed() {
|
function validatePositionsDisplayed() {
|
||||||
cy.getByTestId('tab-positions').should('be.visible');
|
cy.getByTestId('tab-positions').should('be.visible');
|
||||||
cy.getByTestId('tab-positions').within(() => {
|
cy.getByTestId('tab-positions').within(() => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { GraphQLError } from 'graphql';
|
||||||
import type { RouteHandler } from 'cypress/types/net-stubbing';
|
import type { RouteHandler } from 'cypress/types/net-stubbing';
|
||||||
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
|
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
|
||||||
|
|
||||||
@ -30,14 +31,15 @@ export const aliasGQLQuery = (
|
|||||||
req: CyHttpMessages.IncomingHttpRequest,
|
req: CyHttpMessages.IncomingHttpRequest,
|
||||||
operationName: string,
|
operationName: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data?: any
|
data?: any,
|
||||||
|
errors?: Partial<GraphQLError>[]
|
||||||
) => {
|
) => {
|
||||||
if (hasOperationName(req, operationName)) {
|
if (hasOperationName(req, operationName)) {
|
||||||
req.alias = operationName;
|
req.alias = operationName;
|
||||||
if (data !== undefined) {
|
if (data !== undefined || errors !== undefined) {
|
||||||
req.reply({
|
req.reply({
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: { data },
|
body: { ...(data && { data }), ...(errors && { errors }) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
makeDataProvider,
|
||||||
|
marketDataErrorPolicyGuard,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import type { MarketsDataQuery } from './__generated__/markets-data';
|
import type { MarketsDataQuery } from './__generated__/markets-data';
|
||||||
import { MarketsDataDocument } from './__generated__/markets-data';
|
import { MarketsDataDocument } from './__generated__/markets-data';
|
||||||
import type { MarketData } from './market-data-provider';
|
import type { MarketData } from './market-data-provider';
|
||||||
@ -16,4 +19,5 @@ export const marketsDataProvider = makeDataProvider<
|
|||||||
>({
|
>({
|
||||||
query: MarketsDataDocument,
|
query: MarketsDataDocument,
|
||||||
getData,
|
getData,
|
||||||
|
errorPolicyGuard: marketDataErrorPolicyGuard,
|
||||||
});
|
});
|
||||||
|
@ -45,22 +45,22 @@ export interface Position {
|
|||||||
averageEntryPrice: string;
|
averageEntryPrice: string;
|
||||||
marginAccountBalance: string;
|
marginAccountBalance: string;
|
||||||
capitalUtilisation: number;
|
capitalUtilisation: number;
|
||||||
currentLeverage: number;
|
currentLeverage: number | undefined;
|
||||||
decimals: number;
|
decimals: number;
|
||||||
marketDecimalPlaces: number;
|
marketDecimalPlaces: number;
|
||||||
positionDecimalPlaces: number;
|
positionDecimalPlaces: number;
|
||||||
totalBalance: string;
|
totalBalance: string;
|
||||||
assetSymbol: string;
|
assetSymbol: string;
|
||||||
liquidationPrice: string;
|
liquidationPrice: string | undefined;
|
||||||
lowMarginLevel: boolean;
|
lowMarginLevel: boolean;
|
||||||
marketId: string;
|
marketId: string;
|
||||||
marketTradingMode: Schema.MarketTradingMode;
|
marketTradingMode: Schema.MarketTradingMode;
|
||||||
markPrice: string;
|
markPrice: string | undefined;
|
||||||
notional: string;
|
notional: string | undefined;
|
||||||
openVolume: string;
|
openVolume: string;
|
||||||
realisedPNL: string;
|
realisedPNL: string;
|
||||||
unrealisedPNL: string;
|
unrealisedPNL: string;
|
||||||
searchPrice: string;
|
searchPrice: string | undefined;
|
||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ export const getMetrics = (
|
|||||||
const marginAccount = accounts?.find((account) => {
|
const marginAccount = accounts?.find((account) => {
|
||||||
return account.market?.id === market?.id;
|
return account.market?.id === market?.id;
|
||||||
});
|
});
|
||||||
if (!marginAccount || !marginLevel || !market || !marketData) {
|
if (!marginAccount || !marginLevel || !market) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const generalAccount = accounts?.find(
|
const generalAccount = accounts?.find(
|
||||||
@ -102,15 +102,22 @@ export const getMetrics = (
|
|||||||
generalAccount?.balance ?? 0,
|
generalAccount?.balance ?? 0,
|
||||||
decimals
|
decimals
|
||||||
);
|
);
|
||||||
const markPrice = toBigNum(marketData.markPrice, marketDecimalPlaces);
|
|
||||||
|
|
||||||
const notional = (
|
const markPrice = marketData
|
||||||
openVolume.isGreaterThan(0) ? openVolume : openVolume.multipliedBy(-1)
|
? toBigNum(marketData.markPrice, marketDecimalPlaces)
|
||||||
).multipliedBy(markPrice);
|
: undefined;
|
||||||
|
const notional = markPrice
|
||||||
|
? (openVolume.isGreaterThan(0)
|
||||||
|
? openVolume
|
||||||
|
: openVolume.multipliedBy(-1)
|
||||||
|
).multipliedBy(markPrice)
|
||||||
|
: undefined;
|
||||||
const totalBalance = marginAccountBalance.plus(generalAccountBalance);
|
const totalBalance = marginAccountBalance.plus(generalAccountBalance);
|
||||||
const currentLeverage = totalBalance.isEqualTo(0)
|
const currentLeverage = notional
|
||||||
? new BigNumber(0)
|
? totalBalance.isEqualTo(0)
|
||||||
: notional.dividedBy(totalBalance);
|
? new BigNumber(0)
|
||||||
|
: notional.dividedBy(totalBalance)
|
||||||
|
: undefined;
|
||||||
const capitalUtilisation = totalBalance.isEqualTo(0)
|
const capitalUtilisation = totalBalance.isEqualTo(0)
|
||||||
? new BigNumber(0)
|
? new BigNumber(0)
|
||||||
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
|
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
|
||||||
@ -119,19 +126,23 @@ export const getMetrics = (
|
|||||||
const marginSearch = toBigNum(marginLevel.searchLevel, decimals);
|
const marginSearch = toBigNum(marginLevel.searchLevel, decimals);
|
||||||
const marginInitial = toBigNum(marginLevel.initialLevel, decimals);
|
const marginInitial = toBigNum(marginLevel.initialLevel, decimals);
|
||||||
|
|
||||||
const searchPrice = marginSearch
|
const searchPrice = markPrice
|
||||||
.minus(marginAccountBalance)
|
? marginSearch
|
||||||
.dividedBy(openVolume)
|
.minus(marginAccountBalance)
|
||||||
.plus(markPrice);
|
.dividedBy(openVolume)
|
||||||
|
.plus(markPrice)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const liquidationPrice = BigNumber.maximum(
|
const liquidationPrice = markPrice
|
||||||
0,
|
? BigNumber.maximum(
|
||||||
marginMaintenance
|
0,
|
||||||
.minus(marginAccountBalance)
|
marginMaintenance
|
||||||
.minus(generalAccountBalance)
|
.minus(marginAccountBalance)
|
||||||
.dividedBy(openVolume)
|
.minus(generalAccountBalance)
|
||||||
.plus(markPrice)
|
.dividedBy(openVolume)
|
||||||
);
|
.plus(markPrice)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const lowMarginLevel =
|
const lowMarginLevel =
|
||||||
marginAccountBalance.isLessThan(
|
marginAccountBalance.isLessThan(
|
||||||
@ -143,7 +154,7 @@ export const getMetrics = (
|
|||||||
averageEntryPrice: position.averageEntryPrice,
|
averageEntryPrice: position.averageEntryPrice,
|
||||||
marginAccountBalance: marginAccount.balance,
|
marginAccountBalance: marginAccount.balance,
|
||||||
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
|
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
|
||||||
currentLeverage: currentLeverage.toNumber(),
|
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
|
||||||
marketDecimalPlaces,
|
marketDecimalPlaces,
|
||||||
positionDecimalPlaces,
|
positionDecimalPlaces,
|
||||||
decimals,
|
decimals,
|
||||||
@ -152,18 +163,20 @@ export const getMetrics = (
|
|||||||
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
|
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
|
||||||
lowMarginLevel,
|
lowMarginLevel,
|
||||||
liquidationPrice: liquidationPrice
|
liquidationPrice: liquidationPrice
|
||||||
.multipliedBy(10 ** marketDecimalPlaces)
|
? liquidationPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
||||||
.toFixed(0),
|
: undefined,
|
||||||
marketId: market.id,
|
marketId: market.id,
|
||||||
marketTradingMode: market.tradingMode,
|
marketTradingMode: market.tradingMode,
|
||||||
markPrice: marketData.markPrice,
|
markPrice: marketData ? marketData.markPrice : undefined,
|
||||||
notional: notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0),
|
notional: notional
|
||||||
|
? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
||||||
|
: undefined,
|
||||||
openVolume: position.openVolume,
|
openVolume: position.openVolume,
|
||||||
realisedPNL: position.realisedPNL,
|
realisedPNL: position.realisedPNL,
|
||||||
unrealisedPNL: position.unrealisedPNL,
|
unrealisedPNL: position.unrealisedPNL,
|
||||||
searchPrice: searchPrice
|
searchPrice: searchPrice
|
||||||
.multipliedBy(10 ** marketDecimalPlaces)
|
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
||||||
.toFixed(0),
|
: undefined,
|
||||||
updatedAt: position.updatedAt || null,
|
updatedAt: position.updatedAt || null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,7 +49,7 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
|
|||||||
}
|
}
|
||||||
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
|
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
|
||||||
valueFormatted;
|
valueFormatted;
|
||||||
return valueFormatted ? (
|
return valueFormatted && notional ? (
|
||||||
<div className="leading-tight font-mono">
|
<div className="leading-tight font-mono">
|
||||||
<div
|
<div
|
||||||
className={classNames('text-right', signedNumberCssClass(openVolume))}
|
className={classNames('text-right', signedNumberCssClass(openVolume))}
|
||||||
@ -115,15 +115,15 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
valueGetter={({
|
valueGetter={({
|
||||||
data,
|
data,
|
||||||
}: VegaValueGetterParams<Position, 'notional'>) => {
|
}: VegaValueGetterParams<Position, 'notional'>) => {
|
||||||
return data?.notional === undefined
|
return !data?.notional
|
||||||
? undefined
|
? undefined
|
||||||
: toBigNum(data?.notional, data.marketDecimalPlaces).toNumber();
|
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
|
||||||
}}
|
}}
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
data,
|
data,
|
||||||
}: VegaValueFormatterParams<Position, 'notional'>) => {
|
}: VegaValueFormatterParams<Position, 'notional'>) => {
|
||||||
return !data
|
return !data || !data.notional
|
||||||
? undefined
|
? '-'
|
||||||
: addDecimalsFormatNumber(
|
: addDecimalsFormatNumber(
|
||||||
data.notional,
|
data.notional,
|
||||||
data.marketDecimalPlaces
|
data.marketDecimalPlaces
|
||||||
@ -173,6 +173,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
data,
|
data,
|
||||||
}: VegaValueGetterParams<Position, 'markPrice'>) => {
|
}: VegaValueGetterParams<Position, 'markPrice'>) => {
|
||||||
return !data ||
|
return !data ||
|
||||||
|
!data.markPrice ||
|
||||||
data.marketTradingMode ===
|
data.marketTradingMode ===
|
||||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||||
? undefined
|
? undefined
|
||||||
@ -180,14 +181,14 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
}}
|
}}
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
data,
|
data,
|
||||||
node,
|
|
||||||
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
|
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
!data.markPrice ||
|
||||||
data.marketTradingMode ===
|
data.marketTradingMode ===
|
||||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||||
) {
|
) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
@ -220,7 +221,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
}}
|
}}
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
data,
|
data,
|
||||||
node,
|
|
||||||
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
|
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
|
||||||
| string
|
| string
|
||||||
| undefined => {
|
| undefined => {
|
||||||
@ -258,7 +258,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
|||||||
}: VegaValueFormatterParams<Position, 'liquidationPrice'>):
|
}: VegaValueFormatterParams<Position, 'liquidationPrice'>):
|
||||||
| string
|
| string
|
||||||
| undefined => {
|
| undefined => {
|
||||||
if (!data) {
|
if (!data || data?.liquidationPrice === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return addDecimalsFormatNumber(
|
return addDecimalsFormatNumber(
|
||||||
|
@ -2,6 +2,7 @@ import type { ApolloError } from '@apollo/client';
|
|||||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||||
|
|
||||||
const NOT_FOUND = 'NotFound';
|
const NOT_FOUND = 'NotFound';
|
||||||
|
const INTERNAL = 'Internal';
|
||||||
|
|
||||||
const isApolloGraphQLError = (
|
const isApolloGraphQLError = (
|
||||||
error: ApolloError | Error | undefined
|
error: ApolloError | Error | undefined
|
||||||
@ -9,21 +10,31 @@ const isApolloGraphQLError = (
|
|||||||
return !!error && !!(error as ApolloError).graphQLErrors;
|
return !!error && !!(error as ApolloError).graphQLErrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasNotFoundGraphQLErrors = (errors: GraphQLErrors, path?: string) => {
|
const hasNotFoundGraphQLErrors = (errors: GraphQLErrors, path?: string[]) => {
|
||||||
return errors.some(
|
return errors.some(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.extensions &&
|
e.extensions &&
|
||||||
e.extensions['type'] === NOT_FOUND &&
|
e.extensions['type'] === NOT_FOUND &&
|
||||||
(!path || e.path?.[0] === path)
|
(!path || path.every((item, i) => item === e?.path?.[i]))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNotFoundGraphQLError = (
|
export const isNotFoundGraphQLError = (
|
||||||
error: Error | ApolloError | undefined,
|
error: Error | ApolloError | undefined,
|
||||||
path?: string
|
path?: string[]
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
isApolloGraphQLError(error) &&
|
isApolloGraphQLError(error) &&
|
||||||
hasNotFoundGraphQLErrors(error.graphQLErrors, path)
|
hasNotFoundGraphQLErrors(error.graphQLErrors, path)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const marketDataErrorPolicyGuard = (errors: GraphQLErrors) => {
|
||||||
|
const path = ['market', 'data'];
|
||||||
|
return errors.every(
|
||||||
|
(e) =>
|
||||||
|
e.extensions &&
|
||||||
|
e.extensions['type'] === INTERNAL &&
|
||||||
|
(!path || path.every((item, i) => item === e?.path?.[i]))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -19,11 +19,12 @@ import type {
|
|||||||
OperationVariables,
|
OperationVariables,
|
||||||
ApolloQueryResult,
|
ApolloQueryResult,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
ApolloError,
|
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
|
import { ApolloError } from '@apollo/client';
|
||||||
|
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
|
|
||||||
import type { Subscription, Observable } from 'zen-observable-ts';
|
import type { Subscription, Observable } from 'zen-observable-ts';
|
||||||
|
import { waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
type Item = {
|
type Item = {
|
||||||
cursor: string;
|
cursor: string;
|
||||||
@ -108,6 +109,23 @@ const paginatedSubscribe = makeDataProvider<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockErrorPolicyGuard: (errors: GraphQLErrors) => boolean = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => true);
|
||||||
|
const errorGuardedSubscribe = makeDataProvider<
|
||||||
|
QueryData,
|
||||||
|
Data,
|
||||||
|
SubscriptionData,
|
||||||
|
Delta
|
||||||
|
>({
|
||||||
|
query,
|
||||||
|
subscriptionQuery,
|
||||||
|
update,
|
||||||
|
getData,
|
||||||
|
getDelta,
|
||||||
|
errorPolicyGuard: mockErrorPolicyGuard,
|
||||||
|
});
|
||||||
|
|
||||||
const derivedSubscribe = makeDerivedDataProvider(
|
const derivedSubscribe = makeDerivedDataProvider(
|
||||||
[paginatedSubscribe, subscribe],
|
[paginatedSubscribe, subscribe],
|
||||||
combineData,
|
combineData,
|
||||||
@ -537,6 +555,34 @@ describe('data provider', () => {
|
|||||||
expect(lastCallbackArgs[0].totalCount).toBe(100);
|
expect(lastCallbackArgs[0].totalCount).toBe(100);
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errorPolicyGuard should work properly', async () => {
|
||||||
|
const subscription = errorGuardedSubscribe(callback, client);
|
||||||
|
const graphQLError = new GraphQLError(
|
||||||
|
'',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
['market', 'data'],
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
type: 'Internal',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const graphQLErrors = [graphQLError];
|
||||||
|
const error = new ApolloError({ graphQLErrors });
|
||||||
|
|
||||||
|
await rejectQuery(error);
|
||||||
|
const data = generateData(0, 5);
|
||||||
|
await resolveQuery({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
expect(mockErrorPolicyGuard).toHaveBeenNthCalledWith(1, graphQLErrors);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getData).toHaveBeenCalledWith({ data }, undefined)
|
||||||
|
);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('derived data provider', () => {
|
describe('derived data provider', () => {
|
||||||
|
@ -5,7 +5,10 @@ import type {
|
|||||||
OperationVariables,
|
OperationVariables,
|
||||||
TypedDocumentNode,
|
TypedDocumentNode,
|
||||||
FetchResult,
|
FetchResult,
|
||||||
|
ErrorPolicy,
|
||||||
|
ApolloQueryResult,
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
|
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||||
import type { Subscription } from 'zen-observable-ts';
|
import type { Subscription } from 'zen-observable-ts';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import { isNotFoundGraphQLError } from './apollo-client';
|
import { isNotFoundGraphQLError } from './apollo-client';
|
||||||
@ -178,6 +181,7 @@ interface DataProviderParams<
|
|||||||
fetchPolicy?: FetchPolicy;
|
fetchPolicy?: FetchPolicy;
|
||||||
resetDelay?: number;
|
resetDelay?: number;
|
||||||
additionalContext?: Record<string, unknown>;
|
additionalContext?: Record<string, unknown>;
|
||||||
|
errorPolicyGuard?: (graphqlErrors: GraphQLErrors) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,6 +190,9 @@ interface DataProviderParams<
|
|||||||
* @param getData transforms received query data to format that will be stored in data provider
|
* @param getData transforms received query data to format that will be stored in data provider
|
||||||
* @param getDelta transforms delta data to format that will be stored in data provider
|
* @param getDelta transforms delta data to format that will be stored in data provider
|
||||||
* @param fetchPolicy
|
* @param fetchPolicy
|
||||||
|
* @param resetDelay
|
||||||
|
* @param additionalContext add property to the context of the query, ie. 'isEnlargedTimeout'
|
||||||
|
* @param errorPolicyGuard indicate which gql errors can be tolerate
|
||||||
* @returns subscribe function
|
* @returns subscribe function
|
||||||
*/
|
*/
|
||||||
function makeDataProviderInternal<
|
function makeDataProviderInternal<
|
||||||
@ -204,6 +211,7 @@ function makeDataProviderInternal<
|
|||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
resetDelay,
|
resetDelay,
|
||||||
additionalContext,
|
additionalContext,
|
||||||
|
errorPolicyGuard,
|
||||||
}: DataProviderParams<
|
}: DataProviderParams<
|
||||||
QueryData,
|
QueryData,
|
||||||
Data,
|
Data,
|
||||||
@ -248,6 +256,30 @@ function makeDataProviderInternal<
|
|||||||
callbacks.forEach((callback) => notify(callback, updateData));
|
callbacks.forEach((callback) => notify(callback, updateData));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const call = (
|
||||||
|
pagination?: Pagination,
|
||||||
|
policy?: ErrorPolicy
|
||||||
|
): Promise<ApolloQueryResult<QueryData>> =>
|
||||||
|
client
|
||||||
|
.query<QueryData>({
|
||||||
|
query,
|
||||||
|
variables: { ...variables, ...(pagination && { pagination }) },
|
||||||
|
fetchPolicy: fetchPolicy || 'no-cache',
|
||||||
|
context: additionalContext,
|
||||||
|
errorPolicy: policy || 'none',
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (
|
||||||
|
err.graphQLErrors &&
|
||||||
|
errorPolicyGuard &&
|
||||||
|
errorPolicyGuard(err.graphQLErrors)
|
||||||
|
) {
|
||||||
|
return call(pagination, 'ignore');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const load = async (start?: number, end?: number) => {
|
const load = async (start?: number, end?: number) => {
|
||||||
if (!pagination) {
|
if (!pagination) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
@ -276,15 +308,9 @@ function makeDataProviderInternal<
|
|||||||
} else if (!pageInfo?.hasNextPage) {
|
} else if (!pageInfo?.hasNextPage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const res = await client.query<QueryData>({
|
|
||||||
query,
|
const res = await call(paginationVariables);
|
||||||
variables: {
|
|
||||||
...variables,
|
|
||||||
pagination: paginationVariables,
|
|
||||||
},
|
|
||||||
fetchPolicy: fetchPolicy || 'no-cache',
|
|
||||||
context: additionalContext,
|
|
||||||
});
|
|
||||||
const insertionData = getData(res.data, variables);
|
const insertionData = getData(res.data, variables);
|
||||||
const insertionPageInfo = pagination.getPageInfo(res.data);
|
const insertionPageInfo = pagination.getPageInfo(res.data);
|
||||||
({ data, totalCount } = pagination.append(
|
({ data, totalCount } = pagination.append(
|
||||||
@ -313,15 +339,11 @@ function makeDataProviderInternal<
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const paginationVariables = pagination
|
||||||
|
? { first: pagination.first }
|
||||||
|
: undefined;
|
||||||
try {
|
try {
|
||||||
const res = await client.query<QueryData>({
|
const res = await call(paginationVariables);
|
||||||
query,
|
|
||||||
variables: pagination
|
|
||||||
? { ...variables, pagination: { first: pagination.first } }
|
|
||||||
: variables,
|
|
||||||
fetchPolicy: fetchPolicy || 'no-cache',
|
|
||||||
context: additionalContext,
|
|
||||||
});
|
|
||||||
data = getData(res.data, variables);
|
data = getData(res.data, variables);
|
||||||
if (data && pagination) {
|
if (data && pagination) {
|
||||||
if (!(data instanceof Array)) {
|
if (!(data instanceof Array)) {
|
||||||
@ -355,7 +377,7 @@ function makeDataProviderInternal<
|
|||||||
}
|
}
|
||||||
loaded = true;
|
loaded = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isNotFoundGraphQLError(e as Error, 'party')) {
|
if (isNotFoundGraphQLError(e as Error, ['party'])) {
|
||||||
data = getData(null, variables);
|
data = getData(null, variables);
|
||||||
loaded = true;
|
loaded = true;
|
||||||
return;
|
return;
|
||||||
@ -495,7 +517,7 @@ const memoize = <
|
|||||||
Delta,
|
Delta,
|
||||||
Variables extends OperationVariables = OperationVariables
|
Variables extends OperationVariables = OperationVariables
|
||||||
>(
|
>(
|
||||||
fn: (variables?: Variables) => Subscribe<Data, Delta, Variables>
|
fn: () => Subscribe<Data, Delta, Variables>
|
||||||
) => {
|
) => {
|
||||||
const cache: {
|
const cache: {
|
||||||
subscribe: Subscribe<Data, Delta, Variables>;
|
subscribe: Subscribe<Data, Delta, Variables>;
|
||||||
@ -506,7 +528,7 @@ const memoize = <
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return cached.subscribe;
|
return cached.subscribe;
|
||||||
}
|
}
|
||||||
const subscribe = fn(variables);
|
const subscribe = fn();
|
||||||
cache.push({ subscribe, variables });
|
cache.push({ subscribe, variables });
|
||||||
return subscribe;
|
return subscribe;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user