mirror of
https://github.com/muerwre/muerwre.github.io.git
synced 2025-04-25 02:46:39 +07:00
added whole content
This commit is contained in:
parent
1b5df685cb
commit
8b25e0631a
70 changed files with 5962 additions and 19 deletions
79
content/GraphQL/Apollo Client pagination.md
Normal file
79
content/GraphQL/Apollo Client pagination.md
Normal 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,
|
||||
};
|
||||
};
|
||||
```
|
157
content/GraphQL/Refresh token in Apollo client.md
Normal file
157
content/GraphQL/Refresh token in Apollo client.md
Normal 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: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue