-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
157 lines (141 loc) · 4.89 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import humps from 'humps'
import { ACCESS_COOKIE_NAME, REFRESH_COOKIE_NAME } from '../../../constants/cookie'
import { LOGOUT_EVENT } from '../../../constants/events'
import { SERVICES_WITHOUT_TOKEN } from '../../../constants/fetch'
import { TokenTypes } from '../../../constants/token'
import { eventEmitter } from '../../events'
import { buildQueryString } from '../../string'
import { decodeJWT, getToken, isUserTokenValid, refreshAccessToken } from '../../token'
import { BaseAppFetch, RequestOptions } from './types'
/**
*
* Fetch function that handles token refresh and other common use cases.
*
* @description
* This is a **BaseApp** feature.
*
* If you believe your changes should be in the BaseApp, please read the **CONTRIBUTING.md** guide.
*
* @example Fetching data with params
* ```ts
* const data = await baseAppFetch('/users', {
* params: {
* search: 'John Doe',
* },
* })
* ```
*
* @example Fetching data with a POST request
* ```ts
* const data = await baseAppFetch('/users', {
* method: 'POST',
* body: {
* firstName: 'John',
* lastName: 'Doe',
* },
* })
* ```
*
* @example Turning off common utilities
* ```ts
* const data = await baseAppFetch('/users', {
* decamelizeRequestBodyKeys: false,
* decamelizeRequestParamsKeys: false,
* camelizeResponseDataKeys: false,
* stringifyBody: false,
* setContentType: false,
* })
* ```
*
* @example Altering defaults
*
* Consider creating an instance of `baseAppFetch` with your custom defaults to reuse, so you don't have to pass them every time.
*
* ```ts
* export const customBaseAppFetch = (props: BaseAppFetchOptions) => baseAppFetch('/not/authenticated/api/route', {
* baseUrl: 'https://another.api.io',
* servicesWithoutToken: [/^\/not\/authenticated\/api\/route/, /\/another\/public\/route/],
* accessCookieName: "My Custom Access Cookie",
* refreshCookieName: "My Custom Refresh Cookie",
* ...props,
* })
* ```
*/
export const baseAppFetch: BaseAppFetch = async (
path: `/${string}` | '',
{
accessCookieName = ACCESS_COOKIE_NAME,
refreshCookieName = REFRESH_COOKIE_NAME,
baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL,
servicesWithoutToken = SERVICES_WITHOUT_TOKEN,
params = {},
decamelizeRequestBodyKeys = true,
decamelizeRequestParamsKeys = true,
camelizeResponseDataKeys = true,
stringifyBody = true,
setContentType = true,
...options
} = {},
) => {
const url = `${baseUrl}${path}`
const isAuthTokenRequired = !servicesWithoutToken.some((regex) => regex.test(path || ''))
const tokenType = process.env.NEXT_PUBLIC_TOKEN_TYPE as TokenTypes | undefined
const fetchOptions: RequestOptions = {
...options,
headers: {
Accept: 'application/json',
...options.headers,
},
}
// stringify and decamelize request body
if (options.body && stringifyBody) {
const bodyStringify = decamelizeRequestBodyKeys
? JSON.stringify(humps.decamelizeKeys(options.body))
: JSON.stringify(options.body)
fetchOptions.body = bodyStringify
}
// token refresh logic
let authToken = await getToken()
if (authToken && isAuthTokenRequired && tokenType === TokenTypes.jwt) {
const isTokenValid = isUserTokenValid(decodeJWT(authToken))
if (!isTokenValid) {
try {
authToken = await refreshAccessToken(accessCookieName, refreshCookieName)
} catch (error) {
if (eventEmitter.listenerCount(LOGOUT_EVENT)) {
eventEmitter.emit(LOGOUT_EVENT)
}
return Promise.reject(error)
}
}
}
// set Authorization header
if (authToken && isAuthTokenRequired) {
fetchOptions.headers!.Authorization =
tokenType === TokenTypes.jwt ? `Bearer ${authToken}` : `Token ${authToken}`
}
// set content-type header
const methodsToSetContentType = ['POST', 'PUT', 'PATCH']
if (setContentType && methodsToSetContentType.includes(fetchOptions.method || '')) {
fetchOptions.headers!['Content-Type'] = 'application/json'
}
let fetchUrl = url
if (Object.keys(params).length > 0) {
// decamelize request params
const decamelizedParams = decamelizeRequestParamsKeys ? humps.decamelizeKeys(params) : params
const queryString = buildQueryString(decamelizedParams)
fetchUrl = `${url}?${queryString}`
}
try {
// had to override the fetchOptions type because of the GraphQLBody type
const response = await fetch(fetchUrl, fetchOptions as RequestInit)
if (response.headers.get('content-type')?.includes('application/json')) {
const data = await response.json()
// camelize response data
return camelizeResponseDataKeys ? humps.camelizeKeys(data) : data
}
return response
} catch (error) {
return Promise.reject(error)
}
}