node package manager
Easy sharing. Manage teams and permissions with one click. Create a free org ยป

smallorange-gateway

CircleCI

Small Orange Gateway

Simple HTTP gateway for lambdas

This gateway takes care to create a HTTP server, call lambda functions, cache into Redis according to provided strategy and log into cloudWatch.

Sample

Setup

	// used env vars
	process.env.ACCESS_KEY_ID = 'xxxxx'; // (required)
	process.env.SECRET_ACCESS_KEY = 'xxxxx'; // (required)
	process.env.REGION = 'xxxxx'; // (optional)
	process.env.REDIS_URL = 'xxxxx'; // (optional)
	process.env.LOG_GROUP = 'xxxxx'; // (optional)
	process.env.PORT = 8080; // (optional)
	process.env.CACHE_PREFIX = ''; // (optional)
	process.env.CACHE_TTL = 2592000; // time in seconds to live (optional) default: 30 days
	process.env.CACHE_TTR = 7200; // time in seconds to refresh (optional) default: 2 hours
	process.env.CACHE_TIMEOUT = 1000; // time in ms to wait before route to the origin (optional) default: 1 second

	// lambdas manifest
	const lambdas = {
		'/': {
			name: 'functionName' // required,
			cache: {
				enabled: args => args.method === 'GET' && !args.hasExtension && !args.url.query || boolean,
				namespace: args => args.host, // required 
				key: args => args.url.pathname  || string // required
				ttl: number, // optional, override default
				ttr: number // optional, override default
			},
			transformArgs: args => { // it happens immediately after authentication is handled
				args.headers = {
					...args.headers,
					customHeader: 'customHeaderValue'
				};
			},
			transformResponse: (response, req, res) => {
				response.body = ...;
				
				return response;
			},
			transformError: err => {
				return err;
			}
		},
		// or with HTTP verb
		'POST /': {
			name: 'functionName' // required,
			cache: {
				enabled: args => args.method === 'GET' && !args.hasExtension && !args.url.query || boolean,
				namespace: args => args.host, // required 
				key: args => args.url.pathname  || string // required
			},
			transformArgs: args => {
				args.headers = {
					...args.headers,
					customHeader: 'customHeaderValue'
				};
			}
		},
		'/functionName': {
			name: 'functionName', // required
			// pass just params (not all args as described below) to the lambda function
			paramsOnly: true,
			defaults: {
				// default request params, it will be merged with params fetched from req.query, in case of key collision, the latter is going to have precedence
				requestParams: {
					width: 100,
					height: 100
				},
				// default response base64 value, lambda response can override this value, if checked, value will be converted to a buffer before returns to the browser
				responseBase64: true,
				// default response headers, lambda response headers will be merged with this value, in case of key collision, the latter is going to have precedence
				respondeHeaders: {
					'content-type': 'image/png'
				}
			}
		},
		'/local': {
			name: 'functionName', // required,
			local: path.resolve('...path to local function index')	
		},
		'/mocked': {
			mocked: args => 'anything'
		},
		'/authOnly': {
			name: 'functionName' // required,
			auth: {
				enabled: true,
				allowedFields: ['role', 'user', 'loggedAt'], // (optional)
				secret: (payload, params, headers) => 'mySecret' || 'mySecret', // (required)
				token(params, headers) => params.token || headers.authorization // (optional),
				options: {
					/*
					algorithms: List of strings with the names of the allowed algorithms. For instance, ["HS256", "HS384"].
					audience: if you want to check audience (aud), provide a value here
					issuer (optional): string or array of strings of valid values for the iss field.
					ignoreExpiration: if true do not validate the expiration of the token.
					ignoreNotBefore...
					subject: if you want to check subject (sub), provide a value here
					clockTolerance: number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers
					*/
				},
				resolve: (auth, args) => object || Observable<Object> // (optional)
			}
		},
		'/adminOnly': {
			enabled: args => true,
			name: 'functionName' // required,
			auth: {
				// ...
				requiredRoles: ['admin']
			}
		},
		'/adminOrPublic': {
			name: 'functionName' // required,
			auth: {
				// ...
				requiredRoles: ['admin', 'public']
			}
		},
		
		// note: JWT should have role property, like:
		// {
		// 	role: string, // (required)
		// 	...anyOtherParams
		// }

		// full wildcards
		'/*': {
			name: 'functionName' // required,
		},
		'/*/*': {
			name: 'functionName' // required,
		},
		// partial wildcards
		'/*/functionName': {
			name: 'functionName' // required,
		},
		'/*/*/functionName': {
			name: 'functionName' // required,
		}
	};

	const gateway = new Gateway({
		logGroup: 'myAppLogs', // || env.LOG_GROUP
		lambdas,
		redisUrl: 'redis://localhost:6380', // || env.REDIS_URL
		cachePrefix: '', || // env.CACHE_PREFIX
	});

Usage Details

	// for a request like
	GET http://localhost/functionName/resource?string=value&number=2&boolean=true&nulled=null

	// lambda function will receive args like:
	{
		body: {},
		hasExtension: false, // if url ends with .jpg or .png
		headers: {
			//...request headers
		},
		host: 'http://localhost',
		method: 'GET',
		// fetched from req.query
		params: {
			string: 'value',
			number: 2,
			boolean: true,
			nulled: null,
			auth: {} // if enabled
		},
		url: {
			path: '/functionName/resource?string=value&number=2&boolean=true&nulled=null',
			pathname: '/functionName/resource',
			query: 'string=value&number=2&boolean=true&nulled=null'
		},
		uri: '/functionName/resource'
	}

	// or just params if explicity declared at lambdas manifest with "paramsOnly = true":
	{
		string: 'value',
		number: 2,
		boolean: true,
		nulled: null
	}

	// lambdas can responds with just string, or an object with following signature
	{	
		//string or stringified object,
		body: string,
		headers: object,
		base64: boolean,
		statusCode: number // is statusCode >= 400, gateway is going to handle as an error following the Http/1.1 rfc (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
	}

Cache handling

	// you can manually mark cache to refresh making a request like:
	POST http://yourhost/cache
	{
		operation: 'markToRefresh',
		namespace: 'http://localhost'
	}

	// or unset
	POST http://yourhost/cache
	{
		operation: 'unset',
		namespace: 'http://localhost',
		keys: ['/', '/cart']
	}