UNPKG

@silvermine/apigateway-utils

Version:

Utility functions for working with AWS API Gateway

251 lines (196 loc) 6.98 kB
'use strict'; var _ = require('underscore'), codes = require('http-status-codes'), Class = require('class.extend'), APIError = require('./APIError'), CONTENT_TYPES = require('./contentTypes'); module.exports = Class.extend({ init: function() { this._headers = {}; this._status = 200; this._body = {}; this._isJSONPSupported = false; this._cacheDurationSeconds = 0; this._errors = []; this.contentType(CONTENT_TYPES.CONTENT_TYPE_JSON); }, status: function(status) { if (!_.isUndefined(status)) { this._status = status; } return this; }, header: function(key, value) { this._headers[key] = value; return this; }, body: function(o) { this._body = o; return this; }, getBody: function() { return this._body; }, contentType: function(type) { return this.header('Content-Type', type); }, allowCORS: function(origin) { return this.header('Access-Control-Allow-Origin', origin || '*'); }, supportJSONP: function(jsonpCallbackQueryParamName) { this._isJSONPSupported = true; this._jsonpQueryParamName = jsonpCallbackQueryParamName; return this; }, getCacheDurationInSeconds: function() { return this._cacheDurationSeconds; }, cacheForSeconds: function(s) { this._cacheDurationSeconds = Math.max(0, s); return this; }, cacheForMinutes: function(m) { return this.cacheForSeconds(m * 60); }, cacheForHours: function(h) { return this.cacheForMinutes(h * 60); }, redirect: function(url, isPermanent) { this.status(isPermanent ? 301 : 302); this.header('Location', url); return this.body('Found. Redirecting to ' + url); }, addErrors: function(errors) { _.each(errors, function(err) { this.addError(err); }.bind(this)); return this; }, addError: function(err, inheritStatus) { this._errors.push(err); if (inheritStatus) { this.status(err.status()); } return this; }, err: function(title, detail, status, inheritStatus) { var err = new APIError(title, detail, status, this); if (inheritStatus) { this.status(status); } this.addError(err); return err; }, badRequest: function(title, detail) { return this.err(title || 'Invalid request', detail, 400, true); }, unauthorized: function(title, detail) { return this.err(title || 'Unauthorized request', detail, 401, true); }, forbidden: function(title, detail) { return this.err(title || 'Forbidden', detail, 403, true); }, notFound: function(title, detail) { return this.err(title || 'Not found', detail, 404, true); }, unsupportedMediaType: function(title, detail) { return this.err(title || 'Can not return requested media type', detail, 415, true); }, unprocessableEntity: function(title, detail) { return this.err(title || 'Unprocessable entity', detail, 422, true); }, serverError: function(title, detail) { return this.err(title || 'Internal error', detail, 500, true); }, notImplemented: function(title, detail) { return this.err(title || 'Not implemented', detail, 501, true); }, serviceUnavailable: function(title, detail) { return this.err(title || 'Service unavailable', detail, 503, true); }, rss: function(body) { this.contentType(CONTENT_TYPES.CONTENT_TYPE_RSS); return this.body(body); }, html: function(body) { this.contentType(CONTENT_TYPES.CONTENT_TYPE_HTML); return this.body(body); }, okayOrNotFound: function(body, contentType) { if (body) { if (contentType) { this.contentType(contentType); } this.body(body); } else { this.notFound(); } return this; }, toResponse: function(req) { var resp; this._updateBodyWithErrors(); this._updateForJSONP(req); if (req.method() !== 'GET' || this._status >= 500) { // Do not allow non-GET or 5xx responses to be cached. this.cacheForMinutes(0); } this._addCacheHeaders(); resp = { statusCode: this._status, headers: this._headers, body: _.isObject(this._body) ? JSON.stringify(this._body) : this._body, }; if (req.getEvent() && req.getEvent().requestContext && req.getEvent().requestContext.elb) { // If you're running your Lambda behind Application Load Balancer, the ELB/ALB // requires the statusDescription and isBase64Encoded fields. Right now this // library does not support base64 encoded responses, so we just set this to // false and try to get you the right status description. resp.statusDescription = resp.statusCode + ' ' + codes.getStatusText(resp.statusCode); resp.isBase64Encoded = false; // And ELB requires that all header values be strings already, whereas with APIGW // you can have booleans / integers as header values. resp.headers = _.mapObject(resp.headers, function(val) { return String(val); }); } return resp; }, _updateBodyWithErrors: function() { if (_.isEmpty(this._body) && !_.isEmpty(this._errors)) { this.body(_.map(this._errors, function(err) { var o = err.toResponseObject(); console.log('API response includes error: %j', o); // eslint-disable-line no-console return o; })); } }, _addCacheHeaders: function() { var now = new Date(), expiry = new Date(now.getTime() + (this._cacheDurationSeconds * 1000)); if (this._cacheDurationSeconds) { delete this._headers.Pragma; this._headers.Expires = expiry.toUTCString(); this._headers['Cache-Control'] = ('must-revalidate, max-age=' + this._cacheDurationSeconds); } else { this._headers.Expires = 'Thu, 19 Nov 1981 08:52:00 GMT'; this._headers['Cache-Control'] = 'no-cache, max-age=0, must-revalidate'; this._headers.Pragma = 'no-cache'; } }, _updateForJSONP: function(req) { var callback; if (!this._isJSONPSupported || !(_.isObject(this._body) || _.isArray(this._body))) { return; } if (req.hasQueryParam(this._jsonpQueryParamName) && this._isValidJSONPCallback(req.query(this._jsonpQueryParamName))) { callback = req.query(this._jsonpQueryParamName); this.contentType(CONTENT_TYPES.CONTENT_TYPE_JSONP); this._body = 'typeof ' + callback + ' === \'function\' && ' + callback + '(' + JSON.stringify(this._body) + ');'; } }, _isValidJSONPCallback: function(name) { return _.isEmpty(name) ? false : /^[A-Za-z0-9_\\$]+$/.test(name); }, }); _.extend(module.exports, CONTENT_TYPES);