UNPKG

7.14 kBJavaScriptView Raw
1'use strict';
2const h = require('highland'),
3 _ = require('lodash'),
4 nodeUrl = require('url'),
5 https = require('https'),
6 b64 = require('base-64'),
7 pluralize = require('pluralize'),
8 agent = new https.Agent({ rejectUnauthorized: false }), // allow self-signed certs
9 CONTENT_TYPES = {
10 json: 'application/json; charset=UTF-8',
11 text: 'text/plain; charset=UTF-8'
12 };
13
14// isormorphic-fetch sets a global
15require('isomorphic-fetch');
16
17/**
18 * get protocol to determine if we need https agent
19 * @param {string} url
20 * @returns {string}
21 */
22function isSSL(url) {
23 return nodeUrl.parse(url).protocol === 'https:';
24}
25
26/**
27 * catch errors in api calls
28 * @param {Error} error
29 * @return {object}
30 */
31function catchError(error) {
32 return { statusText: error.message };
33}
34
35/**
36 * check status of api calls
37 * note: this happens AFTER catchError, so those errors are dealt with here
38 * @param {object} res
39 * @return {object}
40 */
41function checkStatus(res) {
42 if (res.status && res.status >= 200 && res.status < 400) {
43 return res;
44 } else {
45 // some other error
46 let error = new Error(res.statusText);
47
48 error.response = res;
49 return error;
50 }
51}
52
53/**
54 * perform the http(s) call
55 * @param {string} url
56 * @param {object} options
57 * @return {Promise}
58 */
59function send(url, options) {
60 return fetch(url, options)
61 .catch(catchError)
62 .then(checkStatus);
63}
64
65/**
66 * GET api call
67 * @param {string} url
68 * @param {object} options
69 * @param {object} [options.headers]
70 * @param {string} [options.type] defaults to json, can be json or text
71 * @return {Stream}
72 */
73function get(url, options = {}) {
74 options.type = options.type || 'json';
75 return h(send(url, {
76 method: 'GET',
77 headers: options.headers,
78 agent: isSSL(url) ? agent : null
79 }).then((res) => {
80 if (res instanceof Error) {
81 res.url = url; // capture urls that we error on
82 return res;
83 } else {
84 return res[options.type]();
85 }
86 }));
87}
88
89/**
90 * PUT api call
91 * @param {string} url
92 * @param {object} data
93 * @param {object} options
94 * @param {string} options.key api key
95 * @param {object} [options.headers]
96 * @param {string} [options.type] defaults to json, can be json or text
97 * @return {Stream}
98 */
99function put(url, data, options = {}) {
100 let headers, body;
101
102 if (!options.key) {
103 throw new Error('Please specify API key to do PUT requests against Clay!');
104 }
105
106 options.type = options.type || 'json';
107 headers = _.assign({
108 'Content-Type': CONTENT_TYPES[options.type],
109 Authorization: `Token ${options.key}`
110 }, options.headers);
111
112 // send stringified json, text, or empty (for @publish)
113 if (data && options.type === 'json') {
114 body = JSON.stringify(data);
115 } else if (data) {
116 body = data;
117 } else {
118 body = undefined;
119 }
120
121 return h(send(url, {
122 method: 'PUT',
123 body: body,
124 headers: headers,
125 agent: isSSL(url) ? agent : null
126 }).then((res) => {
127 if (res instanceof Error) {
128 return { type: 'error', details: url, message: res.message };
129 } else {
130 return { type: 'success', message: url };
131 }
132 }));
133 // we don't care about the data returned from the PUT, but we do care it it worked or not
134}
135
136/**
137 * POST to an elastic endpoint with a query
138 * @param {string} url of the endpoint
139 * @param {object} query
140 * @param {object} options
141 * @param {string} options.key
142 * @param {object} [options.headers]
143 * @return {Stream}
144 */
145function query(url, query, options = {}) {
146 let headers;
147
148 if (!options.key) {
149 throw new Error('Please specify API key to do POST requests against Clay!');
150 }
151
152 headers = _.assign({
153 'Content-Type': CONTENT_TYPES.json,
154 Authorization: `Token ${options.key}`
155 }, options.headers);
156
157 return h(send(url, {
158 method: 'POST',
159 body: JSON.stringify(query),
160 headers: headers,
161 agent: isSSL(url) ? agent : null
162 }).then((res) => {
163 if (res instanceof Error) {
164 return { type: 'error', details: url, message: res.message };
165 } else if (_.includes(res.headers.get('content-type'), 'text/html')) {
166 // elastic error, which is returned as 200 and raw text
167 // rather than an error status code and json
168 return res.text().then((str) => {
169 return {
170 type: 'error',
171 message: str.slice(0, str.indexOf(' ::')),
172 details: url,
173 url
174 };
175 });
176 } else {
177 return res.json().then((obj) => {
178 if (_.get(obj, 'hits.total')) {
179 return {
180 type: 'success',
181 message: pluralize('result', _.get(obj, 'hits.total'), true),
182 details: url,
183 data: _.map(_.get(obj, 'hits.hits', []), (hit) => _.assign(hit._source, { _id: hit._id })),
184 total: _.get(obj, 'hits.total')
185 };
186 } else {
187 // no results!
188 return {
189 type: 'error',
190 message: 'No results',
191 details: url,
192 url
193 };
194 }
195 });
196 }
197 }));
198}
199
200/**
201 * try fetching <some prefix>/_uris until it works (or it reaches the bare hostname)
202 * @param {string} currentURL to check
203 * @param {string} publicURI that corresponds with a page uri
204 * @param {object} options
205 * @return {Promise}
206 */
207function recursivelyCheckURI(currentURL, publicURI, options) {
208 let urlArray = currentURL.split('/'),
209 possiblePrefix, possibleUrl;
210
211 urlArray.pop();
212 possiblePrefix = urlArray.join('/');
213 possibleUrl = `${possiblePrefix}/_uris/${b64.encode(publicURI)}`;
214
215 return send(possibleUrl, {
216 method: 'GET',
217 headers: options.headers,
218 agent: isSSL(possibleUrl) ? agent : null
219 }).then((res) => res.text())
220 .then((uri) => ({ uri, prefix: possiblePrefix })) // return page uri and the prefix we discovered
221 .catch(() => {
222 if (possiblePrefix.match(/^https?:\/\/[^\/]*$/)) {
223 return Promise.reject(new Error(`Unable to find a Clay api for ${publicURI}`));
224 } else {
225 return recursivelyCheckURI(possiblePrefix, publicURI, options);
226 }
227 });
228}
229
230/**
231 * given a public url, do GET requests against possible api endpoints until <prefix>/_uris is found,
232 * then do requests against that until a page uri is resolved
233 * note: because of the way Clay mounts sites on top of other sites,
234 * this begins with the longest possible path and cuts it down (via /) until <path>/_uris is found
235 * @param {string} url
236 * @param {object} [options]
237 * @return {Stream}
238 */
239function findURI(url, options = {}) {
240 const parts = nodeUrl.parse(url),
241 publicURI = parts.hostname + parts.pathname;
242
243 return h(recursivelyCheckURI(url, publicURI, options));
244}
245
246/**
247 * determine if url is a proper elastic endpoint prefix
248 * @param {string} url
249 * @return {Stream}
250 */
251function isElasticPrefix(url) {
252 return h(send(`${url}/_components`, {
253 method: 'GET',
254 agent: isSSL(url) ? agent : null
255 }).then((res) => {
256 if (res instanceof Error) {
257 return false;
258 } else {
259 return true;
260 }
261 }));
262}
263
264module.exports.get = get;
265module.exports.put = put;
266module.exports.query = query;
267module.exports.findURI = findURI;
268module.exports.isElasticPrefix = isElasticPrefix;