/
GraphQLConnector.js
231 lines (200 loc) · 6.66 KB
/
GraphQLConnector.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
import crypto from 'crypto';
import DataLoader from 'dataloader';
import rp from 'request-promise';
import { GrampsError } from './errors';
import defaultLogger from '../lib/defaultLogger';
/**
* An abstract class to lay groundwork for data connectors.
*/
export default class GraphQLConnector {
/**
* Bluemix requests require a bearer token. This is retrieved by
* `@console/console-platform-express-session` and stored in `req.user.token`
* for each Express request. This is passed to the class in the config object
* for the `GraphQLConnector` constructor.
* @type {object}
*/
headers = {};
/**
* Set `request-promise` as a class property.
* @type {RequestPromise}
*/
request = rp;
/**
* How long to cache GET requests.
* @type {number}
*/
cacheExpiry = 300;
/**
* If true, GET requests will be cached for `this.cacheExpiry` seconds.
* @type {boolean}
*/
enableCache = true;
redis = false;
logger = defaultLogger;
/**
* Define required props and create an instance of DataLoader.
* @constructs GraphQLConnector
* @param {object} expressRequest the request object from Express
* @return {void}
*/
constructor() {
if (new.target === GraphQLConnector) {
throw new Error('Cannot construct GraphQLConnector classes directly');
}
}
/**
* Get configuration options for `request-promise`.
* @param {string} uri the URI where the request should be sent
* @return {object}
*/
getRequestConfig = uri => ({
uri,
json: true,
resolveWithFullResponse: true,
headers: { ...this.headers },
});
/**
* Executes a request for data from a given URI
* @param {string} uri the URI to load
* @return {Promise} resolves with the loaded data; rejects with errors
*/
getRequestData = uri =>
new Promise((resolve, reject) => {
this.logger.info(`Request made to ${uri}`);
const toHash = `${uri}-${this.headers.Authorization}`;
const key = crypto
.createHash('md5')
.update(toHash)
.digest('hex');
const hasCache = this.enableCache && this.getCached(key, resolve, reject);
this.request(this.getRequestConfig(uri))
.then(response => {
const data = response.body;
// If the data came through alright, cache it.
if (response.statusCode === 200) {
this.addToCache(key, data);
}
return data;
})
.then(response => !hasCache && resolve(response))
.catch(error => {
const err = GrampsError({
error,
description: `There was an error with the query: ${error.message}`,
docsLink: 'https://ibm.biz/graphql',
errorCode: 'GRAPHQL_QUERY_ERROR',
graphqlModel: this.constructor.name,
targetEndpoint: uri,
});
reject(err);
});
});
/**
* Loads an array of URIs
* @param {Array} uris an array of URIs to request data from
* @return {Promise} the response from all requested URIs
*/
load = uris => Promise.all(uris.map(this.getRequestData));
/**
* Stores given data in the cache for a set amount of time.
* @param {string} key an MD5 hash of the request URI
* @param {object} response the data to be cached
* @return {object} the response, unchanged
*/
addToCache(key, response) {
if (this.redis && this.enableCache) {
this.logger.info(`caching response data for ${this.cacheExpiry} seconds`);
this.redis.setex(key, this.cacheExpiry, JSON.stringify(response));
}
return response;
}
/**
* Loads data from the cache, if available.
* @param {string} key the cache identifier key
* @param {function} successCB typically a Promise’s `resolve` function
* @param {function} errorCB typically a Promise’s `reject` function
* @return {boolean} true if cached data was found, false otherwise
*/
getCached(key, successCB, errorCB) {
if (!this.redis) {
return;
}
this.redis.get(key, (error, data) => {
if (error) {
errorCB(error);
}
// If we have data, initiate a refetch in the background and return it.
if (data !== null) {
this.logger.info('loading data from cache');
// The success callback will typically resolve a Promise.
successCB(JSON.parse(data));
return true;
}
return false;
});
}
/**
* Configures and sends a GET request to a REST API endpoint.
* @param {string} endpoint the API endpoint to send the request to
* @param {object} config optional configuration for the request
* @return {Promise} Promise that resolves with the request result
*/
get(endpoint) {
this.createLoader();
return this.loader.load(`${this.apiBaseUri}${endpoint}`);
}
/**
* Helper method for sending non-cacheable requests.
*
* @see https://github.com/request/request-promise
*
* @param {string} endpoint the API endpoint to hit
* @param {string} method the HTTP request method to use
* @param {object} options config options for request-promise
* @return {Promise} result of the request
*/
mutation(endpoint, method, options) {
const config = {
// Start with our baseline configuration.
...this.getRequestConfig(`${this.apiBaseUri}${endpoint}`),
// Add some PUT-specific options.
method,
// Allow the caller to override options.
...options,
};
return this.request(config);
}
/**
* Configures and sends a POST request to a REST API endpoint.
* @param {string} endpoint the API endpoint to send the request to
* @param {object} body optional body to be sent with the request
* @param {object} config optional configuration for request-promise
* @return {Promise} Promise that resolves with the request result
*/
post(endpoint, body = {}, options = {}) {
return this.mutation(endpoint, 'POST', {
body,
...options,
});
}
/**
* Configures and sends a PUT request to a REST API endpoint.
* @param {string} endpoint the API endpoint to send the request to
* @param {object} body optional body to be sent with the request
* @param {object} config optional configuration for request-promise
* @return {Promise} Promise that resolves with the request result
*/
put(endpoint, body = {}, options = {}) {
return this.mutation(endpoint, 'PUT', {
body,
...options,
});
}
createLoader() {
// We can enable batched queries later on, which may be more performant.
this.loader = new DataLoader(this.load, {
batch: false,
});
}
}