-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
/
api.js
138 lines (116 loc) · 3.7 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
import { normalize, schema } from 'normalizr'
import { camelizeKeys } from 'humps'
// Extracts the next page URL from Github API response.
const getNextPageUrl = response => {
const link = response.headers.get('link')
if (!link) {
return null
}
const nextLink = link.split(',').find(s => s.indexOf('rel="next"') > -1)
if (!nextLink) {
return null
}
return nextLink.trim().split(';')[0].slice(1, -1)
}
const API_ROOT = 'https://api.github.com/'
// Fetches an API response and normalizes the result JSON according to schema.
// This makes every API response have the same shape, regardless of how nested it was.
const callApi = (endpoint, schema) => {
const fullUrl =
endpoint.indexOf(API_ROOT) === -1 ? API_ROOT + endpoint : endpoint
return fetch(fullUrl).then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json)
}
const camelizedJson = camelizeKeys(json)
const nextPageUrl = getNextPageUrl(response)
return Object.assign({}, normalize(camelizedJson, schema), {
nextPageUrl
})
})
)
}
// We use this Normalizr schemas to transform API responses from a nested form
// to a flat form where repos and users are placed in `entities`, and nested
// JSON objects are replaced with their IDs. This is very convenient for
// consumption by reducers, because we can easily build a normalized tree
// and keep it updated as we fetch more data.
// Read more about Normalizr: https://github.com/paularmstrong/normalizr
// GitHub's API may return results with uppercase letters while the query
// doesn't contain any. For example, "someuser" could result in "SomeUser"
// leading to a frozen UI as it wouldn't find "someuser" in the entities.
// That's why we're forcing lower cases down there.
const userSchema = new schema.Entity(
'users',
{},
{
idAttribute: user => user.login.toLowerCase()
}
)
const repoSchema = new schema.Entity(
'repos',
{
owner: userSchema
},
{
idAttribute: repo => repo.fullName.toLowerCase()
}
)
// Schemas for Github API responses.
export const Schemas = {
USER: userSchema,
USER_ARRAY: [userSchema],
REPO: repoSchema,
REPO_ARRAY: [repoSchema]
}
// Action key that carries API call info interpreted by this Redux middleware.
export const CALL_API = 'Call API'
// A Redux middleware that interprets actions with CALL_API info specified.
// Performs the call and promises when such actions are dispatched.
export default store => next => action => {
const callAPI = action[CALL_API]
if (typeof callAPI === 'undefined') {
return next(action)
}
let { endpoint } = callAPI
const { schema, types } = callAPI
if (typeof endpoint === 'function') {
endpoint = endpoint(store.getState())
}
if (typeof endpoint !== 'string') {
throw new Error('Specify a string endpoint URL.')
}
if (!schema) {
throw new Error('Specify one of the exported Schemas.')
}
if (!Array.isArray(types) || types.length !== 3) {
throw new Error('Expected an array of three action types.')
}
if (!types.every(type => typeof type === 'string')) {
throw new Error('Expected action types to be strings.')
}
const actionWith = data => {
const finalAction = Object.assign({}, action, data)
delete finalAction[CALL_API]
return finalAction
}
const [requestType, successType, failureType] = types
next(actionWith({ type: requestType }))
return callApi(endpoint, schema).then(
response =>
next(
actionWith({
response,
type: successType
})
),
error =>
next(
actionWith({
type: failureType,
error: error.message || 'Something bad happened'
})
)
)
}