Skip to content

Commit 2996a3f

Browse files
authored
Merge pull request #70 from jeremydaly/v0.9.0
v0.9.0
2 parents 262f377 + 40b49d9 commit 2996a3f

17 files changed

+1413
-1078
lines changed

Diff for: README.md

+36-2
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,13 @@ Whatever you decide is best for your use case, **Lambda API** is there to suppor
9494
- [Middleware](#middleware)
9595
- [Clean Up](#clean-up)
9696
- [Error Handling](#error-handling)
97+
- [Error Types](#error-types)
98+
- [Error Logging](#error-logging)
9799
- [Namespaces](#namespaces)
98100
- [CORS Support](#cors-support)
99101
- [Lambda Proxy Integration](#lambda-proxy-integration)
100102
- [Configuring Routes in API Gateway](#configuring-routes-in-api-gateway)
103+
- [TypeScript Support](#typescript-support)
101104
- [Contributions](#contributions)
102105

103106
## Installation
@@ -118,6 +121,7 @@ Require the `lambda-api` module into your Lambda handler script and instantiate
118121
| callbackName | `String` | Override the default callback query parameter name for JSONP calls |
119122
| logger | `boolean` or `object` | Enables default [logging](#logging) or allows for configuration through a [Logging Configuration](#logging-configuration) object. |
120123
| mimeTypes | `Object` | Name/value pairs of additional MIME types to be supported by the `type()`. The key should be the file extension (without the `.`) and the value should be the expected MIME type, e.g. `application/json` |
124+
| serializer | `Function` | Optional object serializer function. This function receives the `body` of a response and must return a string. Defaults to `JSON.stringify` |
121125
| version | `String` | Version number accessible via the `REQUEST` object |
122126

123127

@@ -129,6 +133,9 @@ const api = require('lambda-api')({ version: 'v1.0', base: 'v1' });
129133
## Recent Updates
130134
For detailed release notes see [Releases](https://github.com/jeremydaly/lambda-api/releases).
131135

136+
### v0.9: New error types, custom serializers, and TypeScript support
137+
Lambda API now generates typed errors for easier parsing in middleware. You can also supply your own custom serializer for formatting output rather than using the default `JSON.stringify`. And thanks to @hassankhan, a TypeScript declaration file is now available.
138+
132139
### v0.8: Logging Support with Sampling
133140
Lambda API has added a powerful (and customizable) logging engine that utilizes native JSON support for CloudWatch Logs. Log entries can be manually added using standard severities like `info` and `warn`. In addition, "access logs" can be automatically generated with detailed information about each requests. See [Logging](#logging) for more information about logging and auto sampling for request tracing.
134141

@@ -390,6 +397,9 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway.
390397
- `rawBody`: If the `isBase64Encoded` flag is `true`, this is a copy of the original, base64 encoded body
391398
- `route`: The matched route of the request
392399
- `requestContext`: The `requestContext` passed from the API Gateway
400+
- `pathParameters`: The `pathParameters` passed from the API Gateway
401+
- `stageVariables`: The `stageVariables` passed from the API Gateway
402+
- `isBase64Encoded`: The `isBase64Encoded` boolean passed from the API Gateway
393403
- `auth`: An object containing the `type` and `value` of an authorization header. Currently supports `Bearer`, `Basic`, `OAuth`, and `Digest` schemas. For the `Basic` schema, the object is extended with additional fields for username/password. For the `OAuth` schema, the object is extended with key/value pairs of the supplied OAuth 1.0 values.
394404
- `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces))
395405
- `cookies`: An object containing cookies sent from the browser (see the [cookie](#cookiename-value-options) `RESPONSE` method)
@@ -950,7 +960,7 @@ const api = require('lambda-api')({
950960
})
951961
```
952962

953-
Additional rules can be added by specify a `rules` parameter in the `sampling` configuration object. The `rules` should contain an `array` of "rule" objects with the following properties:
963+
Additional rules can be added by specifying a `rules` parameter in the `sampling` configuration object. The `rules` should contain an `array` of "rule" objects with the following properties:
954964

955965
| Property | Type | Description | Default | Required |
956966
| -------- | ---- | ----------- | ------- | -------- |
@@ -1123,7 +1133,23 @@ api.use(errorHandler1,errorHandler2)
11231133
**NOTE:** Error handling middleware runs on *ALL* paths. If paths are passed in as the first parameter, they will be ignored by the error handling middleware.
11241134

11251135
### Error Types
1126-
Error logs are generate using either the `error` or `fatal` logging level. Errors can be triggered from within routes and middleware by calling the `error()` method on the `RESPONSE` object. If provided a `string` as an error message, this will generate an `error` level log entry. If you supply a JavaScript `Error` object, or you `throw` an error, a `fatal` log entry will be generated.
1136+
Lambda API provides several different types of errors that can be used by your application. `RouteError`, `MethodError`, `ResponseError`, and `FileError` will all be passed to your error middleware. `ConfigurationError`s will throw an exception when you attempt to `.run()` your route and can be caught in a `try/catch` block. Most error types contain additional properties that further detail the issue.
1137+
1138+
```javascript
1139+
const errorHandler = (err,req,res,next) => {
1140+
1141+
if (err.name === 'RouteError') {
1142+
// do something with route error
1143+
} else if (err.name === 'FileError') {
1144+
// do something with file error
1145+
}
1146+
// continue
1147+
next()
1148+
})
1149+
```
1150+
1151+
### Error Logging
1152+
Error logs are generated using either the `error` or `fatal` logging level. Errors can be triggered from within routes and middleware by calling the `error()` method on the `RESPONSE` object. If provided a `string` as an error message, this will generate an `error` level log entry. If you supply a JavaScript `Error` object, or you `throw` an error, a `fatal` log entry will be generated.
11271153

11281154
```javascript
11291155
api.get('/somePath', (res,req) => {
@@ -1271,6 +1297,14 @@ Simply create a `{proxy+}` route that uses the `ANY` method and all requests wil
12711297
## Reusing Persistent Connections
12721298
If you are using persistent connections in your function routes (such as AWS RDS or Elasticache), be sure to set `context.callbackWaitsForEmptyEventLoop = false;` in your main handler. This will allow the freezing of connections and will prevent Lambda from hanging on open connections. See [here](https://www.jeremydaly.com/reuse-database-connections-aws-lambda/) for more information.
12731299

1300+
## TypeScript Support
1301+
An `index.d.ts` declaration file has been included for use with your TypeScript projects (thanks @hassankhan). Please feel free to make suggestions and contributions to keep this up-to-date with future releases.
1302+
1303+
```javascript
1304+
// import Lambda API and TypeScript declarations
1305+
import API from 'lambda-api'
1306+
```
1307+
12741308
## Contributions
12751309
Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports or create a pull request.
12761310

Diff for: index.d.ts

+50-45
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,42 @@ import {
55
} from 'aws-lambda';
66

77
declare interface CookieOptions {
8-
domain: string;
9-
expires: Date;
10-
httpOnly: boolean;
11-
maxAge: number;
12-
path: string;
13-
secure: boolean;
14-
sameSite: boolean | 'Strict' | 'Lax';
8+
domain?: string;
9+
expires?: Date;
10+
httpOnly?: boolean;
11+
maxAge?: number;
12+
path?: string;
13+
secure?: boolean;
14+
sameSite?: boolean | 'Strict' | 'Lax';
1515
}
1616

1717
declare interface CorsOptions {
18-
credentials: boolean;
19-
exposeHeaders: string;
20-
headers: string;
21-
maxAge: number;
22-
methods: string;
23-
origin: string;
18+
credentials?: boolean;
19+
exposeHeaders?: string;
20+
headers?: string;
21+
maxAge?: number;
22+
methods?: string;
23+
origin?: string;
2424
}
2525

2626
declare interface FileOptions {
27-
maxAge: number;
28-
root: string;
29-
lastModified: boolean | string;
30-
headers: {};
31-
cacheControl: boolean | string;
32-
private: boolean;
27+
maxAge?: number;
28+
root?: string;
29+
lastModified?: boolean | string;
30+
headers?: {};
31+
cacheControl?: boolean | string;
32+
private?: boolean;
3333
}
3434

3535
declare interface App {
3636
[namespace: string]: HandlerFunction;
3737
}
3838
declare type ErrorCallback = (error?: Error) => void;
39-
declare type HandlerFunction = (req: Request, res) => void | {} | Promise<{}>;
39+
declare type HandlerFunction = (req: Request, res: Response) => void | {} | Promise<{}>;
4040
declare type LoggerFunction = (message: string) => void;
4141
declare type NextFunction = () => void;
4242
declare type TimestampFunction = () => string;
43+
declare type SerializerFunction = (body: object) => string;
4344
declare type FinallyFunction = (req: Request, res: Response) => void;
4445

4546
declare type METHODS = 'GET'
@@ -52,44 +53,45 @@ declare type METHODS = 'GET'
5253
| 'ANY';
5354

5455
declare interface SamplingOptions {
55-
route: string;
56-
target: number;
57-
rate: number
58-
period: number;
59-
method: string | string[];
56+
route?: string;
57+
target?: number;
58+
rate?: number
59+
period?: number;
60+
method?: string | string[];
6061
}
6162

6263
declare interface LoggerOptions {
63-
access: boolean | string;
64-
customKey: string;
65-
detail: boolean;
66-
level: string;
67-
levels: {
64+
access?: boolean | string;
65+
customKey?: string;
66+
detail?: boolean;
67+
level?: string;
68+
levels?: {
6869
[key: string]: string;
6970
};
70-
messageKey: string;
71-
nested: boolean;
72-
timestamp: boolean | TimestampFunction;
73-
sampling: {
74-
target: number;
75-
rate: number;
76-
period: number;
77-
rules: SamplingOptions[];
71+
messageKey?: string;
72+
nested?: boolean;
73+
timestamp?: boolean | TimestampFunction;
74+
sampling?: {
75+
target?: number;
76+
rate?: number;
77+
period?: number;
78+
rules?: SamplingOptions[];
7879
};
79-
serializers: {
80+
serializers?: {
8081
[name: string]: (req: Request) => {};
8182
};
82-
stack: boolean;
83+
stack?: boolean;
8384
}
8485

8586
declare interface Options {
86-
base: string;
87-
callbackName: string;
88-
logger: boolean | LoggerOptions;
89-
mimeTypes: {
87+
base?: string;
88+
callbackName?: string;
89+
logger?: boolean | LoggerOptions;
90+
mimeTypes?: {
9091
[key: string]: string;
9192
};
92-
version: string;
93+
serializer?: SerializerFunction;
94+
version?: string;
9395
}
9496

9597
declare class Request {
@@ -110,6 +112,9 @@ declare class Request {
110112
rawBody: string;
111113
route: '';
112114
requestContext: APIGatewayEventRequestContext;
115+
isBase64Encoded: boolean;
116+
pathParameters: { [name: string]: string } | null;
117+
stageVariables: { [name: string]: string } | null;
113118
auth: {
114119
[key: string]: any;
115120
type: 'Bearer' | 'Basic' | 'OAuth' | 'Digest';

Diff for: index.js

+12-8
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
/**
44
* Lightweight web framework for your serverless applications
55
* @author Jeremy Daly <[email protected]>
6-
* @version 0.8.1
6+
* @version 0.9.0
77
* @license MIT
88
*/
99

10-
const REQUEST = require('./lib/request.js') // Resquest object
11-
const RESPONSE = require('./lib/response.js') // Response object
12-
const UTILS = require('./lib/utils.js') // Require utils library
13-
const LOGGER = require('./lib/logger.js') // Require logger library
10+
const REQUEST = require('./lib/request') // Resquest object
11+
const RESPONSE = require('./lib/response') // Response object
12+
const UTILS = require('./lib/utils') // Require utils library
13+
const LOGGER = require('./lib/logger') // Require logger library
1414
const prettyPrint = require('./lib/prettyPrint') // Pretty print for debugging
15+
const { ConfigurationError } = require('./lib/errors') // Require custom errors
1516

1617
// Create the API class
1718
class API {
@@ -24,6 +25,7 @@ class API {
2425
this._base = props && props.base && typeof props.base === 'string' ? props.base.trim() : ''
2526
this._callbackName = props && props.callback ? props.callback.trim() : 'callback'
2627
this._mimeTypes = props && props.mimeTypes && typeof props.mimeTypes === 'object' ? props.mimeTypes : {}
28+
this._serializer = props && props.serializer && typeof props.serializer === 'function' ? props.serializer : JSON.stringify
2729

2830
// Set sampling info
2931
this._sampleCounts = {}
@@ -90,7 +92,7 @@ class API {
9092
METHOD(method, path, handler) {
9193

9294
if (typeof handler !== 'function') {
93-
throw new Error(`No route handler specified for ${method} method on ${path} route.`)
95+
throw new ConfigurationError(`No route handler specified for ${method} method on ${path} route.`)
9496
}
9597

9698
// Ensure method is an array
@@ -147,7 +149,7 @@ class API {
147149
async run(event,context,cb) {
148150

149151
// Set the event, context and callback
150-
this._event = event
152+
this._event = event || {}
151153
this._context = this.context = typeof context === 'object' ? context : {}
152154
this._cb = cb ? cb : undefined
153155

@@ -212,6 +214,8 @@ class API {
212214
// Catch all async/sync errors
213215
async catchErrors(e,response,code,detail) {
214216

217+
// console.log('\n\n------------------------\n',e,'\n------------------------\n\n');
218+
215219
// Error messages should never be base64 encoded
216220
response._isBase64 = false
217221

@@ -306,7 +310,7 @@ class API {
306310
} else if (arguments[arg].length === 4) {
307311
this._errors.push(arguments[arg])
308312
} else {
309-
throw new Error('Middleware must have 3 or 4 parameters')
313+
throw new ConfigurationError('Middleware must have 3 or 4 parameters')
310314
}
311315
}
312316
}

Diff for: lib/errors.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict'
2+
3+
/**
4+
* Lightweight web framework for your serverless applications
5+
* @author Jeremy Daly <[email protected]>
6+
* @license MIT
7+
*/
8+
9+
// Custom error types
10+
11+
class RouteError extends Error {
12+
constructor(message,path) {
13+
super(message)
14+
this.name = this.constructor.name
15+
this.path = path
16+
}
17+
}
18+
19+
class MethodError extends Error {
20+
constructor(message,method,path) {
21+
super(message)
22+
this.name = this.constructor.name
23+
this.method = method
24+
this.path = path
25+
}
26+
}
27+
28+
class ConfigurationError extends Error {
29+
constructor(message) {
30+
super(message)
31+
this.name = this.constructor.name
32+
}
33+
}
34+
35+
class ResponseError extends Error {
36+
constructor(message,code) {
37+
super(message)
38+
this.name = this.constructor.name
39+
this.code = code
40+
}
41+
}
42+
43+
class FileError extends Error {
44+
constructor(message,err) {
45+
super(message)
46+
this.name = this.constructor.name
47+
for (let e in err) this[e] = err[e]
48+
}
49+
}
50+
51+
// Export the response object
52+
module.exports = {
53+
RouteError,
54+
MethodError,
55+
ConfigurationError,
56+
ResponseError,
57+
FileError
58+
}

Diff for: lib/logger.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference
1212

1313
const UTILS = require('./utils') // Require utils library
14+
const { ConfigurationError } = require('./errors') // Require custom errors
1415

1516
// Config logger
1617
exports.config = (config,levels) => {
@@ -21,7 +22,7 @@ exports.config = (config,levels) => {
2122
if (cfg.levels && typeof cfg.levels === 'object') {
2223
for (let lvl in cfg.levels) {
2324
if (!/^[A-Za-z_]\w*$/.test(lvl) || isNaN(cfg.levels[lvl])) {
24-
throw new Error('Invalid level configuration')
25+
throw new ConfigurationError('Invalid level configuration')
2526
}
2627
}
2728
levels = Object.assign(levels,cfg.levels)
@@ -225,7 +226,7 @@ const parseSamplerConfig = (config,levels) => {
225226
let cfg = typeof config === 'object' ? config : config === true ? {} : false
226227

227228
// Error on invalid config
228-
if (cfg === false) throw new Error('Invalid sampler configuration')
229+
if (cfg === false) throw new ConfigurationError('Invalid sampler configuration')
229230

230231
// Create rule default
231232
let defaults = (inputs) => {
@@ -244,7 +245,7 @@ const parseSamplerConfig = (config,levels) => {
244245
let rules = Array.isArray(cfg.rules) ? cfg.rules.map((rule,i) => {
245246
// Error if missing route or not a string
246247
if (!rule.route || typeof rule.route !== 'string')
247-
throw new Error('Invalid route specified in rule')
248+
throw new ConfigurationError('Invalid route specified in rule')
248249

249250
// Parse methods into array (if not already)
250251
let methods = (Array.isArray(rule.method) ? rule.method :

0 commit comments

Comments
 (0)