added whole content

This commit is contained in:
Fedor Katurov 2022-11-03 10:38:11 +06:00
parent 1b5df685cb
commit 8b25e0631a
70 changed files with 5962 additions and 19 deletions

View file

@ -0,0 +1,79 @@
Say, we have `gql` response like this and we wan't to have pagination with it. Let's merge it as it specified in [official documentation](https://www.apollographql.com/docs/react/caching/cache-field-behavior/#the-merge-function)
```graphql
query listItems(
filter: Filter,
sort: String,
limit: Number,
offset: Number,
): ItemList!
input Filter {
name: String!
type: String!
}
type ItemList {
items: [Item!]!
totalCount: Int!
}
```
We will setup `ApolloClient` with `typePolicies` to merge incoming data in cache:
```typescript
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
// ...
cache: new InMemoryCache({ typePolicies }),
// ...
});
export const typePolicies: TypePolicies = {
Query: {
fields: {
// query name
listItems: {
// apollo will serialize and use keyArgs as unique
// identifier in cache for every query
// consider choosing the right fields,
// i.e. limit and offset won't work here
keyArgs: [
'sort', // primitive type
'filter', ['name', 'type'] // nested fields of `filter`
],
merge: mergeItemsWithTotalCount,
},
}
}
```
We will need merge function `mergeItemsWithTotalCount`, which will join results of query and cached data for specific key:
```typescript
/** merges all sources with { items: unknown[], totalCount: number } */
const mergeItemsWithTotalCount = (existing, incoming, { args }) => {
// no existing data
if (!existing || !args?.offset || args.offset < existing.length) {
return incoming || [];
}
// If hook was called multiple times
if (existing?.items?.length && args?.offset < existing.items.length) {
return existing || [];
}
// merge cache and incoming data
const items = [...(existing?.items || []), ...(incoming?.items || [])];
// apply latest result for totalCount
const totalCount = incoming?.totalCount || existing?.totalCount;
return {
...(incoming || existing || {}),
items,
totalCount,
};
};
```

View file

@ -0,0 +1,157 @@
If your GraphQL api needs token refresh option, you can pass custom fetch function for Apollo Client.
```typescript
export const createApolloClient = (
url: string,
logout: () => void,
getAuthorizationData: () => { authorization: string },
refreshToken: () => Promise<
{ accessToken: string; refreshToken: string } | undefined
>,
) =>
new ApolloClientBase({
// ...other options
link: ApolloLink.from([
// ...other options
setContext(async (_, { headers }) => {
return {
headers: {
...headers,
...getAuthorizationData(),
},
};
}),
new HttpLink({
uri: url,
fetch: fetchWithTokenRefresh(logout, refreshToken),
}),
]),
});
```
Custom fetch function for this request. You should tune `hasUnauthorizedError` and
`isRefreshRequestOptions` to match your api.
```typescript
/** Global singleton for refreshing promise */
let refreshingPromise: Promise<string> | null = null;
/** Checks if GraphQl errors has unauthenticated error */
const hasUnauthorizedError = (errors: Array<{ code?: ErrorCode }>): boolean =>
Array.isArray(errors) &&
errors.some(error => {
return error.status === 401; // Distinguish unauthorized error here
});
/** Detects if customFetch is sending refresh request */
const isRefreshRequestOptions = (options: RequestInit) => {
try {
const body = JSON.parse(options?.body as string);
return body.operationName === 'RefreshToken';
} catch (e) {
return false;
}
};
/** fetchWithTokenRefresh is a custom fetch function with token refresh for apollo */
export const fetchWithTokenRefresh =
(
logout: () => void,
refreshToken: () => Promise<
{ accessToken: string; refreshToken: string } | undefined
>,
) =>
async (uri: string, options: RequestInit): Promise<Response> => {
// already refreshing token, wait for it and then use refreshed token
// or use empty authorization if refreshing failed
if (
!isRefreshRequestOptions(options) &&
refreshingPromise &&
(options.headers as Record<string, string>)?.authorization
) {
const newAccessToken = await refreshingPromise
.catch(() => {
// refreshing token from other request failed, retry without authorization
return '';
});
options.headers = {
...(options.headers || {}),
authorization: newAccessToken,
};
}
return fetch(uri, options).then(async response => {
const text = await response.text();
const json = JSON.parse(text);
// check for unauthorized errors, if not present, just return result
if (
isRefreshRequestOptions(options) ||
!json?.errors ||
!Array.isArray(json.errors) ||
!hasUnauthorizedError(json.errors)
) {
return {
...response,
ok: true,
json: async () =>
new Promise<unknown>(resolve => {
resolve(json);
}),
text: async () =>
new Promise<string>(resolve => {
resolve(text);
}),
};
}
// If unauthorized, refresh token and try again
if (!refreshingPromise) {
refreshingPromise = refreshToken()
.then(async (tokens): Promise<string> => {
refreshingPromise = null;
if (!tokens?.accessToken) {
throw new Error('Session expired');
}
return tokens?.accessToken;
})
.catch(() => {
refreshingPromise = null;
// can't refresh token. logging out
logout();
throw new Error('Session expired');
});
}
// success or any non-auth error
return refreshingPromise
.then(async (newAccessToken: string) => {
// wait for other request's refreshing query to finish, when retry
return fetch(uri, {
...options,
headers: {
...(options.headers || {}),
authorization: newAccessToken,
},
});
})
.catch(async () => {
// refreshing token from other request failed, retry without authorization
return fetch(uri, {
...options,
headers: {
...(options.headers || {}),
authorization: '',
},
});
});
});
};
```