/
H.common.js
369 lines (335 loc) · 10.7 KB
/
H.common.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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
'use strict';
const qs = require('qs');
/**
* H helper functions available in both Servers and browsers
* @name H
*/
module.exports = (H) => {
/**
* Checks if variable is an array (equivalent to Array.isArray)
* @memberof H
* @param {Mixed} variable Variable to check
* @returns {Boolean}
*/
H.isArray = Array.isArray;
/**
* Checks if variable is an object and not an array
* @memberof H
* @param {Mixed} variable Variable to check
* @returns {Boolean}
*/
H.isObject = obj=>obj===Object(obj) && !H.isArray(obj);
// Time
/**
* Wait for a number of miliseconds
* @async
* @memberof H
* @param {Number} time Time in miliseconds
* @returns {Promise}
*/
H.delay = (time) => new Promise(function(resolve, reject) {setTimeout(resolve, time);});
/**
* Returns current timestamp in miliseconds
* @returns {Number} timestamp in miliseconds
*/
H.timestampMs = () => +new Date();
/**
* Returns current timestamp in seconds
* @returns {Number} timestamp in seconds
*/
H.timestamp = () => Math.round(+new Date()/1000);
/**
* Converts a timestamp into relative time. E.g. about 2 hours ago; less than a minute; in about 5 minutes
* @param {Number|Date} timestamp Time
* @returns {String} relative representation of timestamp
*/
H.relativeTime = (timestamp) => {
var seconds = Math.floor((new Date())/1000 - (parseInt(timestamp instanceof Date?(timestamp/1000):timestamp) || 0)),
words = '',
interval = 0,
intervals = {
year: seconds / 31536000,
month: seconds / 2592000,
day: seconds / 86400,
hour: seconds / 3600,
minute: seconds / 60
};
if(seconds == 0)
words = 'now';
else if (seconds>0) {
var ranges = {
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'one day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years'
};
var distance = 'less than a minute';
for (var key in intervals) {
interval = Math.floor(intervals[key]);
if (interval > 1) {
distance = ranges[key + 's'];
break;
} else if (interval === 1) {
distance = ranges[key];
break;
}
}
distance = distance.replace(/%d/i, interval);
words += distance + ' ago';
} else {
seconds = Math.abs(seconds);
var ranges = {
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: '1 day',
days: '%d days',
};
var distance = 'less than a minute';
for (var key in intervals) {
interval = Math.floor(Math.abs(intervals[key]));
if(!ranges.hasOwnProperty(key))
continue;
if (interval > 1) {
distance = ranges[key + 's'];
break;
} else if (interval === 1) {
distance = ranges[key];
break;
}
}
distance = distance.replace(/%d/i, interval);
words += 'in '+distance;
}
return words.trim();
};
/**
* Converts a string into a handlized format (uppercase letters, numbers and dashes)
* @param {String} str String to handlize
* @returns {String} Handle
*/
H.handlize = (str) => String(str).toLowerCase().replace(/[^a-z0-9]/g, ' ').replace(/\s+/g, ' ').trim().replace(/\s/g, '-');
/**
* Helper regular expressions (RegExp)
* handle : Valid handle (lowercase letters, numbers, underscores and dashes).
* email : Valid email address
*/
H.regexp = {
handle : /^[a-z0-9_-]+$/,
email : /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
};
/**
* Escapes a regular expression string (RegExp)
* s : String to escape
*/
H.regexpEscape = s=>s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
/**
* Determines if an object has a property. (uses Object.prototype for security)
* @param {Object} obj Object to check
* @param {String} key Property to check
* @returns {Bool} True if obj has the key property
*/
H.hasOwnProp = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
/**
* Goes through an object and returns value of a specific path (using dot notation)
* @param {Object} obj Input object
* @param {String} path Path to traverse (dot notation). e.g. parent.children.property
* @returns {Mixed} Value of the path element
*/
H.getVariable = (obj, path) => {
var pointer = obj;
if(!pointer)
return undefined;
var failed = String(path).split('.').find((p) => {
if((H.isObject(pointer) || H.isArray(pointer)) && H.hasOwnProp(pointer, p))
pointer = pointer[p];
else
return true;
});
if(typeof failed!='undefined')
return undefined;
return pointer;
};
/**
* Goes through an object and sets the value of a specific path (using dot notation)
* @param {Object} obj Input object
* @param {String} path Path to traverse (dot notation). e.g. parent.children.property
* @param {Mixed} value New value to inject
*/
H.setVariable = (obj, path, value) => {
var pointer = obj;
path = String(path).split('.');
var last = path.pop();
var n = path.length;
var failed = path.find((p, i) => {
if(!H.hasOwnProp(pointer, p))
pointer[p] = {};
if(!H.isObject(pointer[p]))
pointer[p] = {};
pointer = pointer[p];
});
pointer[last] = value;
return true;
};
// HTML
/**
* Escapes a string for HTML injection
* @param {String} str Input string
* @returns {String} Cleaned output
*/
H.escape = (str) => {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// HTTP Requests
/**
* Requests an HTTP endpoint
* @async
* @param {String} method Method to use (GET|POST|PUT|DELETE|HEAD)
* @param {String} url HTTP endpoint
* @param {Object} payload Payload to inject (will be converted to query string in case of GET request otherwise, the payload is sent as a JSON body)
* @param {Object} headers Headers to inject
* @param {Object} extras extra options for the request (same as fetch API options)
* @param {String} [inFormat="json"] Format of the input request (json, form).
* @param {String} [outFormat="json"] Format of the output response (json, text, buffer, stream).
* @returns {Promise<String>} Response body
*/
H.httpRequest = async (method='GET', url, payload={}, headers={}, extras={}, inFormat='json', outFormat='json') => {
var getPayload = '';
method = method.toUpperCase(); // making sure...
if(method=='GET') {
getPayload = '?'+qs.stringify(payload);
if(getPayload=='?')
getPayload = '';
}
var rethrow = false;
try {
var body = undefined;
if(method!='GET') {
if(inFormat=='json')
body = JSON.stringify(payload);
else if(inFormat=='form') {
body = new URLSearchParams(qs.stringify(payload));
}
}
if(inFormat=='json') {
if(!headers['Content-Type'] && !headers['content-type'])
headers['Content-Type'] = 'application/json';
if(!headers['Accept'] && !headers['accept'])
headers['Accept'] = 'application/json';
}
var response = await H._fetch(url+getPayload, {
method,
body,
headers,
});
if(response.ok) {
try {
let contentType = (response.headers.get('content-type') || '').split(';').shift();
if(outFormat=='json')// || contentType=='application/json')
return await response.json(); // Expect all responses to be in JSON format
else if (outFormat=='buffer')
return await response.buffer();
else if(outFormat=='stream')
return response.body;
return await response.text();
} catch(e) {
rethrow = true;
throw new Error('The server returned an invalid response.');
}
} else {
rethrow = true;
let contentType = (response.headers.get('content-type') || '').split(';').shift();
let response_ = response;
try {
if(contentType=='application/json')
response_ = await response.json();
} catch(e) {}
if(response.status != 200) {
throw new H.Error('Error '+response.status+(response_.message?': '+response_.message:(response.statusText?': '+response.statusText:'')), response.status, response_);
} else
throw new H.Error('A network error occured.', response.status, response_);
}
} catch(e) {
if(rethrow)
throw e;
throw new H.Error('Could not reach server. '+e.toString());
}
};
/**
* Requests a GET HTTP endpoint
* @async
* @param {String} url HTTP endpoint
* @param {Object} payload Payload to inject will be converted to query string
* @param {Object} headers Headers to inject
* @param {Object} extras extra options for request (same as fetch API options)
* @param {String} [inFormat="json"] Format of the input request (json, form).
* @param {String} [outFormat="json"] Format of the output response (json, text, buffer, stream).
* @returns {Promise<String>} Response body
*/
H.httpGet = async function(){return await H.httpRequest('GET', ...arguments);};
/**
* Requests a POST HTTP endpoint
* @async
* @param {String} url HTTP endpoint
* @param {Object} payload Payload to inject
* @param {Object} headers Headers to inject
* @param {Object} extras extra options for request (same as fetch API options)
* @param {String} [inFormat="json"] Format of the input request (json, form).
* @param {String} [outFormat="json"] Format of the output response (json, text, buffer, stream).
* @returns {Promise<String>} Response body
*/
H.httpPost = async function(){return await H.httpRequest('POST', ...arguments);};
/**
* Requests a PUT HTTP endpoint
* @async
* @param {String} url HTTP endpoint
* @param {Object} payload Payload to inject
* @param {Object} headers Headers to inject
* @param {Object} extras extra options for request (same as fetch API options)
* @param {String} [inFormat="json"] Format of the input request (json, form).
* @param {String} [outFormat="json"] Format of the output response (json, text, buffer, stream).
* @returns {Promise<String>} Response body
*/
H.httpPut = async function(){return await H.httpRequest('PUT', ...arguments);};
/**
* Requests a DELETE HTTP endpoint
* @async
* @param {String} url HTTP endpoint
* @param {Object} payload Payload to inject
* @param {Object} headers Headers to inject
* @param {Object} extras extra options for request (same as fetch API options)
* @param {String} [inFormat="json"] Format of the input request (json, form).
* @param {String} [outFormat="json"] Format of the output response (json, text, buffer, stream).
* @returns {Promise<String>} Response body
*/
H.httpDelete = async function(){return await H.httpRequest('DELETE', ...arguments);};
class ErrorMessage extends Error {
constructor(message, code=500, response) {
super(message);
this.errorMessage = message;
this.statusCode = code;
this.response = response;
}
toString() {
return this.errorMessage;
}
}
/**
* Custom Error constructor
* @param {String} message Error message
* @param {Number} code Error code.
*/
H.Error = ErrorMessage;
}