UNPKG

15.8 kBJavaScriptView Raw
1/*
2 * The MIT License (MIT)
3 *
4 * Copyright (c) 2015 Mailjet
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 *
24 */
25
26const DEBUG_MODE = false
27const RESOURCE = 0
28const ID = 1
29const ACTION = 2
30
31/*
32 * Imports.
33 *
34 * qs is used to format the url from the provided parameters and method
35 * _path will join a path according to the OS specifications
36 * https will be used to make a secure http request to the API
37 * fs will simply be used to read files
38 */
39
40const qs = require('querystring')
41const request = require('superagent')
42const _path = require('path')
43const JSONb = require('json-bigint')({ storeAsString: true })
44const version = require('./package.json').version
45
46/* Extend superagent request with proxy method */
47require('superagent-proxy')(request)
48
49function setValueIfExist(targetObject, path, value) {
50 if(value) {
51 targetObject[path] = value;
52 }
53}
54
55/*
56 * MailjetClient constructor.
57 *
58 * @qpi_key (optional) {String} mailjet account api key
59 * @api_secret (optional) {String} mailjet account api secret
60 * @options (optional) {Object} additional connection options
61 *
62 * If you don't know what this is about, sign up to Mailjet at:
63 * https://www.mailjet.com/
64 */
65function MailjetClient (api_key, api_secret, options, perform_api_call) {
66 return this.authStrategy(api_key, api_secret, options, perform_api_call)
67}
68
69/**
70 * @param (optional){String} api_key || api_token
71 * @param (optional){String} api_secret
72 * @param (optional){Object} options
73 * @param (optional){any} perform_api_call
74 */
75MailjetClient.prototype.authStrategy = function(api_key, api_secret, options, perform_api_call) {
76
77 var isTokenRequired = this.isTokenRequired(api_key, api_secret, options, perform_api_call)
78 var self = this
79 // Check if api version requires toekn authentication
80 // This is one of the approaches, maybe there is better
81 if (isTokenRequired) {
82 // params are shifted one position to left as we don't have api secret any more
83 // api_key becomes api_token
84 // api_secret becomes options
85 // options becomes perform_api_call
86 return tokenAuthentication(api_key, api_secret, options)
87 } else {
88 // params are in correct order
89 return basicAuthentication(api_key, api_secret, options, perform_api_call)
90 }
91
92 /**
93 *
94 * @param (optional){String} api_key mailjet api key
95 * @param (optional){String} api_secret mailjet api secret
96 * @param (optional){Object} options additional connection options
97 * @param (optional){boolean} perform_api_call
98 */
99 function basicAuthentication(api_key, api_secret, options, perform_api_call) {
100
101 self.config = self.setConfig(options)
102 self.perform_api_call = perform_api_call || false
103 // To be updated according to the npm repo version
104 self.version = version
105 if (api_key && api_secret) {
106 self.connect(api_key, api_secret, options)
107 }
108
109 return self
110 }
111
112 /**
113 *
114 * @param (optional){String} api_token mailjet api token
115 * @param (optional){Object} options additional connection options
116 * @param (optional){boolean} perform_api_call
117 */
118 function tokenAuthentication(api_token, options, perform_api_call) {
119 self.perform_api_call = perform_api_call || false
120 // To be updated according to the npm repo version
121 self.version = version
122 if (api_token) {
123 self.connect(api_token, options)
124 }
125
126 return self
127 }
128}
129
130MailjetClient.prototype.isTokenRequired = function () {
131 var args = [].slice.call(arguments)
132 var vals = args.filter(a => a !== undefined)
133
134 if (DEBUG_MODE) {
135 console.log('Defined arguments: ' + JSON.stringify(vals))
136 }
137
138 return (vals.length === 1 || (vals.length >= 2 && typeof vals[1] === 'object'))
139}
140
141MailjetClient.prototype.typeJson = function (body) {
142 var keys = Object.keys(body)
143 for (var i in keys) {
144 var key = keys[i]
145 body[key] = parseInt(body[key]) || body[key]
146 }
147 return body
148}
149
150/*
151 * [Static] connect.
152 *
153 * Return a nez connected instance of the MailjetClient class
154 *
155 * @k {String} mailjet qpi key
156 * @s {String} mailjet api secret
157 * @o {String} optional connection options
158 *
159 */
160MailjetClient.connect = function (k, s, o) {
161 return new MailjetClient().connect(k, s, o)
162}
163
164/*
165 * connect.
166 *
167 * create a auth property from the api key and secret
168 *
169 * @apiKey || @apiToken {String}
170 * @apiSecret {String}
171 * @options {Object}
172 *
173 */
174MailjetClient.prototype.connect = function (apiKey, apiSecret, options) {
175 return this.connectStrategy(apiKey, apiSecret, options)
176}
177
178/**
179 * @param (optional){String} apiKey || apiToken
180 * @param (optional){String} apiSecret
181 * @param (optional){Object} options
182 */
183MailjetClient.prototype.connectStrategy = function (apiKey, apiSecret, options) {
184
185 var self = this
186 var isTokenRequired = this.isTokenRequired(apiKey, apiSecret, options)
187
188 if (isTokenRequired) {
189 return tokenConnectStrategy(apiKey, apiSecret)
190 } else {
191 return basicConnectStrategy(apiKey, apiSecret, options)
192 }
193
194 function basicConnectStrategy(apiKey, apiSecret, options) {
195 if (!apiKey) {
196 throw new Error('Mailjet API_KEY is required');
197 }
198 if (!apiSecret) {
199 throw new Error('Mailjet API_SECRET is required');
200 }
201
202 setOptions(options)
203 self.apiKey = apiKey
204 self.apiSecret = apiSecret
205 return self
206 }
207
208 function tokenConnectStrategy(apiToken, options) {
209 setOptions(options)
210 if (!apiToken) {
211 throw new Error('Mailjet API_TOKEN is required');
212 }
213
214 self.apiToken = apiToken
215 return self
216 }
217
218 function setOptions(options) {
219 self.options = options || {}
220 if (self.options) {
221 self.config = self.setConfig(options)
222 }
223 }
224}
225
226MailjetClient.prototype.setConfig = function (options) {
227 const config = require('./config.json')
228 if (typeof options === 'object' && options != null && options.length != 0) {
229 if (options.url) config.url = options.url
230 if (options.version) config.version = options.version
231 if (options.perform_api_call) config.perform_api_call = options.perform_api_call
232 } else if (options != null) {
233 throw new Error('warning, your options variable is not a valid object.')
234 }
235
236 return config
237}
238
239/*
240 * path.
241 *
242 * Returns a formatted url from a http method and
243 * a parameters object literal
244 *
245 * @resource {String}
246 * @sub {String} REST/''/DATA
247 * @params {Object literal} {name: value}
248 *
249 */
250MailjetClient.prototype.path = function (resource, sub, params, options) {
251 if (DEBUG_MODE) {
252 console.log('resource =', resource)
253 console.log('subPath =', sub)
254 console.log('filters =', params)
255 }
256
257 const url = (options && 'url' in options) ? options.url : this.config.url
258 const api_version = (options && 'version' in options) ? options.version : this.config.version
259
260 const base = _path.join(api_version, sub)
261 if (Object.keys(params).length === 0) {
262 return _path.join(url, base + '/' + resource)
263 }
264
265 const querystring = qs.stringify(params);
266 return _path.join(url, base + '/' + resource + '?' + querystring)
267}
268
269/*
270 * httpRequest.
271 *
272 * @method {String} http method (GET/POST...)
273 * @url {String} url path to be used for the request
274 * @data {Object literal} additional data espacially for POST/PUT operations
275 * @callback -optional {Function} called on response from the server, or on error
276 *
277 * @return a promise triggering 'success' on response
278 * and error on error
279 */
280
281MailjetClient.prototype.httpRequest = function (method, url, data, callback, perform_api_call){
282 var req = request[method](url)
283 .set('user-agent', 'mailjet-api-v3-nodejs/' + this.version)
284
285 .set('Content-type', url.indexOf('text:plain') > -1
286 ? 'text/plain'
287 : 'application/json')
288
289 if (this.apiToken) {
290 req.set('Authorization', 'Bearer ' + this.apiToken)
291 } else {
292 req.auth(this.apiKey, this.apiSecret)
293 }
294
295 if (this.options.proxyUrl) {
296 req = req.proxy(this.options.proxyUrl)
297 }
298 if (this.options.timeout) {
299 req = req.timeout(this.options.timeout)
300 }
301
302 const payload = method === 'post' || method === 'put' ? data : {}
303
304 if (DEBUG_MODE) {
305 console.log('Final url: ' + url)
306 console.log('body: ' + JSON.stringify(payload))
307 }
308
309 if (perform_api_call === false || this.perform_api_call) {
310 return Promise.resolve({body: payload, url: url})
311 }
312
313 if (method === 'delete') { method = 'del' }
314 if (method === 'post' || method === 'put') { req = req.send(data) }
315
316 return new Promise(function (resolve, reject) {
317
318 const ret = function (err, result) {
319 return typeof callback === 'function'
320 ? callback(err, result)
321 : err
322 ? reject(err)
323 : resolve(result)
324 }
325
326 req.end(function (err, result) {
327 var body
328
329 try {
330 body = JSONb.parse(result.text)
331 } catch (e) {
332 body = {}
333 }
334
335 if (err) {
336 const error = new Error()
337 error.ErrorMessage = body.ErrorMessage || err.message
338
339 error.statusCode = err.status || null
340 error.response = result || null
341 error.message = `Unsuccessful: Status Code: "${error.statusCode}" Message: "${error.ErrorMessage}"`
342
343 setValueIfExist(error, 'ErrorIdentifier', body.ErrorIdentifier)
344 setValueIfExist(error, 'ErrorCode', body.ErrorCode)
345 setValueIfExist(error, 'ErrorRelatedTo', body.ErrorRelatedTo)
346
347 return ret(error)
348 }
349
350 return ret(null, {
351 response: result,
352 body: body
353 })
354 })
355 })
356}
357
358/*
359 *
360 * MailjetResource constructor
361 *
362 * This class creates a function that can be build through method chaining
363 *
364 * @method {String} http method
365 * @func {String} resource/path to be sent
366 * @context {MailjetClient[instance]} parent client
367 */
368function MailjetResource (method, func, options, context) {
369 this.base = func
370 this.callUrl = func
371 this.options = options || context.options
372
373 this.resource = func.toLowerCase()
374
375 this.lastAdded = RESOURCE
376 var self = context
377
378 /*
379 It can be REST or nothing if we only know the resource
380 */
381 this.subPath = (function () {
382 if (func.toLowerCase() !== 'send' && func.indexOf('sms') === -1) {
383 return 'REST'
384 }
385 return ''
386 })()
387
388 /**
389 *
390 * result.
391 *
392 * @params (optional) {Object Littteral} parameters to be sent to the server
393 * @callback (optional) {Function} called on response or error
394 */
395 var that = this
396 this.result = function (params, callback) {
397 params = params || {}
398 if (typeof params === 'function') {
399 callback = params
400 params = {}
401 }
402
403 /*
404 We build the querystring depending on the parameters. if the user explicitly mentionned
405 a filters property, we pass it to the url
406 */
407 var path = self.path(that.callUrl, that.subPath, (function () {
408 if (params.filters) {
409 var ret = params.filters
410 delete params.filters
411 return ret
412 } else if (method === 'get') {
413 return params
414 } else {
415 return {}
416 }
417 })(), that.options)
418
419 var perform_api_call = null
420 if (that.options && 'perform_api_call' in that.options) {
421 perform_api_call = that.options.perform_api_call
422 } else {
423 perform_api_call = self.config.perform_api_call
424 }
425
426 that.callUrl = that.base
427 self.lastAdded = RESOURCE
428 return self.httpRequest(method, 'https://' + path, params, callback, perform_api_call)
429 }
430}
431
432/**
433 *
434 * id.
435 *
436 * Add an ID and prevent invalid id chaining
437 *
438 * @value {String/Number} append an id to the path
439 * @return the MailjetResource instance to allow method chaining
440 *
441 */
442MailjetResource.prototype.id = function (value) {
443 if (this.lastAdded === ID && DEBUG_MODE) {
444 console.warn('[WARNING] your request may fail due to invalid id chaining')
445 }
446
447 this.callUrl = _path.join(this.callUrl, value.toString())
448 this.lastAdded = ID
449 return this
450}
451
452/**
453 *
454 * action.
455 *
456 * Add an Action and prevent invalid action chaining
457 *
458 * @value {String} append an action to the path
459 * @return the MailjetResource instance to allow method chaining
460 *
461 */
462MailjetResource.prototype.action = function (name) {
463 if (this.lastAdded === ACTION && DEBUG_MODE) {
464 console.warn('[WARNING] your request may fail due to invalid action chaining')
465 }
466
467 this.callUrl = _path.join(this.callUrl, name)
468 this.action = name.toLowerCase()
469
470 this.lastAdded = ACTION
471
472 if (this.action.toLowerCase() === 'csvdata') {
473 this.action = 'csvdata/text:plain'
474 } else if (this.action.toLowerCase() === 'csverror') {
475 this.action = 'csverror/text:csv'
476 }
477
478 var self = this
479 this.subPath = (function () {
480 if (self.resource === 'contactslist' && self.action === 'csvdata/text:plain' ||
481 self.resource === 'batchjob' && self.action === 'csverror/text:csv') {
482 return 'DATA'
483 } else {
484 return self.subPath
485 }
486 })()
487 return self
488}
489
490/**
491 *
492 * request.
493 *
494 * @parmas {Object literal} method parameters
495 * @callback (optional) {Function} triggered when done
496 *
497 * @return {String} the server response
498 */
499
500MailjetResource.prototype.request = function (params, callback) {
501 return this.result(params, callback).catch(function (err) {
502 try {
503 const ErrorDetails = JSON.parse(err.response.text);
504 const fullMessage = ErrorDetails.Messages[0].Errors[0].ErrorMessage;
505
506 if (typeof fullMessage === "string") {
507 err.message = err.message + ";\n" + fullMessage;
508 throw err;
509 }
510 throw err;
511 } catch {
512 throw err;
513 }
514 });
515};
516
517/*
518 * post.
519 *
520 * @func {String} required Mailjet API function to be used (can contain a whole action path)
521 *
522 * @returns a function that make an httpRequest for each call
523 */
524MailjetClient.prototype.post = function (func, options) {
525 return new MailjetResource('post', func, options, this)
526}
527
528/*
529 * get.
530 *
531 * @func {String} required Mailjet API function to be used (can contain a whole action path)
532 *
533 * @returns a function that make an httpRequest for each call
534 */
535MailjetClient.prototype.get = function (func, options) {
536 return new MailjetResource('get', func, options, this)
537}
538
539/*
540 * delete.
541 *
542 * @func {String} required Mailjet API function to be used (can contain a whole action path)
543 *
544 * @returns a function that make an httpRequest for each call
545 */
546MailjetClient.prototype.delete = function (func, options) {
547 return new MailjetResource('delete', func, options, this)
548}
549
550/*
551 * put.
552 *
553 * @func {String} required Mailjet API function to be used (can contain a whole action path)
554 *
555 * @returns a function that make an httpRequest for each call
556 */
557MailjetClient.prototype.put = function (func, options) {
558 return new MailjetResource('put', func, options, this)
559}
560
561/*
562 * Exports the Mailjet client.
563 *
564 * you can require it like so:
565 * var mj = require ('./mailjet-client')
566 *
567 * or for the bleeding edge developpers out there:
568 * import mj from './mailjet-client'
569 */
570module.exports = MailjetClient