/
api.js
269 lines (227 loc) · 7.54 KB
/
api.js
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/**
* API Functions
*
* React Native Starter App
* https://github.com/mcnamee/react-native-starter-app
*/
/* global fetch console */
import DeviceInfo from 'react-native-device-info';
import JWT from '@lib/api.jwt';
// Consts and Libs
import { AppConfig, ErrorMessages, APIConfig } from '@constants/';
import AppUtil from '@lib/util';
// We'll use JWT for API Authentication
// const Token = {};
const Token = new JWT();
// Config
const ENDPOINTS = APIConfig.endpoints;
let USER_AGENT;
try {
// Build user agent string
USER_AGENT = `${AppConfig.appName} ` +
`${DeviceInfo.getVersion()}; ${DeviceInfo.getSystemName()} ` +
`${DeviceInfo.getSystemVersion()}; ${DeviceInfo.getBrand()} ` +
`${DeviceInfo.getDeviceId()}`;
} catch (e) {
USER_AGENT = `${AppConfig.appName}`;
}
// Enable debug output when in Debug mode
const DEBUG_MODE = AppConfig.DEV;
// Number each API request (used for debugging)
let requestCounter = 0;
/* Helper Functions ==================================================================== */
/**
* Debug or not to debug
*/
function debug(str, title) {
if (DEBUG_MODE && (title || str)) {
if (title) {
console.log(`=== DEBUG: ${title} ===========================`);
}
if (str) {
console.log(str);
console.log('%c ...', 'color: #CCC');
}
}
}
/**
* Sends requests to the API
*/
function handleError(err) {
let error = '';
if (typeof err === 'string') error = err;
else if (err && err.message) error = err.message;
else if (err && err.error && err.error.message) error = err.error.message;
if (!error) error = ErrorMessages.default;
return error;
}
/**
* Convert param object into query string
* eg.
* {foo: 'hi there', bar: { blah: 123, quux: [1, 2, 3] }}
* foo=hi there&bar[blah]=123&bar[quux][0]=1&bar[quux][1]=2&bar[quux][2]=3
*/
function serialize(obj, prefix) {
const str = [];
Object.keys(obj).forEach((p) => {
const k = prefix ? `${prefix}[${p}]` : p;
const v = obj[p];
str.push((v !== null && typeof v === 'object') ?
serialize(v, k) :
`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
});
return str.join('&');
}
/**
* Sends requests to the API
*/
function fetcher(method, inputEndpoint, inputParams, inputBody) {
let endpoint = inputEndpoint;
let body = inputBody;
const params = inputParams;
return new Promise(async (resolve, reject) => {
requestCounter += 1;
const requestNum = requestCounter;
// After x seconds, let's call it a day!
const timeoutAfter = 7;
const apiTimedOut = setTimeout(() => (
reject(ErrorMessages.timeout)
), timeoutAfter * 1000);
if (!method || !endpoint) return reject('Missing params (AppAPI.fetcher).');
// Build request
const req = {
method: method.toUpperCase(),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': USER_AGENT,
},
};
// Add Token to authEndpoints only
if (Token.getToken) {
// Form array of complete auth endpoints to test against
const authEndpoints = APIConfig.authEnpoints.map(x => APIConfig.endpoints.get(x));
// Only add the token when we need to
if (authEndpoints.indexOf(endpoint) > -1) {
// Get the token
const apiToken = await Token.getToken();
if (apiToken) {
// Add token to header
if (APIConfig.sendAuthorizationBearerHeader) {
req.headers.Authorization = `Bearer ${apiToken}`;
}
// Add token to body
if (method !== 'GET' && APIConfig.sendTokenInBody) {
if (!body) {
body = {};
body[APIConfig.sendTokenInBodyKey] = apiToken;
} else if (typeof body === 'object') {
body[APIConfig.sendTokenInBodyKey] = apiToken;
}
}
}
// Add token to URL (as a query param)
if (APIConfig.sendTokenInUrl) {
if (typeof params === 'object') {
params[APIConfig.sendTokenInUrlKey] = apiToken;
}
}
}
}
// Add Endpoint Params
let urlParams = '';
if (params) {
// Object - eg. /recipes?title=this&cat=2
if (typeof params === 'object') {
// Replace matching params in API routes eg. /recipes/{param}/foo
Object.keys(params).forEach((param) => {
if (endpoint.includes(`{${param}}`)) {
endpoint = endpoint.split(`{${param}}`).join(params[param]);
delete params[param];
}
});
// Check if there's still an 'id' prop, /{id}?
if (params.id !== undefined) {
if (typeof params.id === 'string' || typeof params.id === 'number') {
urlParams = `/${params.id}`;
delete params.id;
}
}
// Add the rest of the params as a query string
urlParams = `?${serialize(params)}`;
// String or Number - eg. /recipes/23
} else if (typeof params === 'string' || typeof params === 'number') {
urlParams = `/${params}`;
// Something else? Just log an error
} else {
debug('You provided params, but it wasn\'t an object!', endpoint + urlParams);
}
}
// Add Body
if (body) req.body = JSON.stringify(body);
const thisUrl = endpoint + urlParams;
debug('', `API Request #${requestNum} to ${thisUrl}`);
if (DEBUG_MODE) console.log(req);
// Make the request
return fetch(thisUrl, req)
.then(async (rawRes) => {
// API got back to us, clear the timeout
clearTimeout(apiTimedOut);
let jsonRes = {};
try {
jsonRes = await rawRes.json();
} catch (error) {
const err = { message: ErrorMessages.invalidJson };
throw err;
}
// Only continue if the header is successful
if (rawRes && rawRes.status === 200) { return jsonRes; }
throw jsonRes;
})
.then((res) => {
debug(res, `API Response #${requestNum} from ${thisUrl}`);
return resolve(res);
})
.catch((err) => {
// API got back to us, clear the timeout
clearTimeout(apiTimedOut);
const apiCredentials = Token.getStoredCredentials ? Token.getStoredCredentials() : {};
// If unauthorized, try logging them back in
if (
!AppUtil.objIsEmpty(apiCredentials) &&
err &&
err.data &&
err.data.status.toString().charAt(0) === 4 &&
err.code !== 'jwt_auth_failed' &&
Token.getToken
) {
return Token.getToken()
.then(() => { fetcher(method, endpoint, params, body); })
.catch(error => reject(error));
}
debug(err, endpoint + urlParams);
return reject(err);
});
});
}
/* Create the API Export ==================================================================== */
/**
* Build services from Endpoints
* - So we can call AppAPI.recipes.get() for example
*/
const AppAPI = {
handleError,
getToken: Token.getToken,
deleteToken: Token.deleteToken,
};
ENDPOINTS.forEach((endpoint, key) => {
AppAPI[key] = {
get: (params, payload) => fetcher('GET', endpoint, params, payload),
post: (params, payload) => fetcher('POST', endpoint, params, payload),
patch: (params, payload) => fetcher('PATCH', endpoint, params, payload),
put: (params, payload) => fetcher('PUT', endpoint, params, payload),
delete: (params, payload) => fetcher('DELETE', endpoint, params, payload),
};
});
/* Export ==================================================================== */
export default AppAPI;