@contextualize/lambda-router 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +63 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +68 -0
- package/dist/handler.d.ts +19 -0
- package/dist/handler.js +87 -0
- package/dist/healthcheck.d.ts +19 -0
- package/dist/healthcheck.js +38 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +17 -0
- package/dist/path.d.ts +26 -0
- package/dist/path.js +107 -0
- package/dist/request.d.ts +20 -0
- package/dist/request.js +172 -0
- package/dist/response.d.ts +13 -0
- package/dist/response.js +34 -0
- package/dist/router.d.ts +20 -0
- package/dist/router.js +76 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +86 -0
- package/package.json +37 -0
- package/readme.md +65 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const _1 = require(".");
|
|
7
|
+
const stream_1 = require("stream");
|
|
8
|
+
class DummyWritable extends stream_1.Writable {
|
|
9
|
+
data = [];
|
|
10
|
+
result;
|
|
11
|
+
resolve;
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.result = new Promise(resolve => {
|
|
15
|
+
this.resolve = resolve;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
_write(chunk, encoding, callback) {
|
|
19
|
+
this.data.push(chunk.toString());
|
|
20
|
+
callback();
|
|
21
|
+
}
|
|
22
|
+
_final(callback) {
|
|
23
|
+
this.resolve(this.data.join(''));
|
|
24
|
+
callback();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function main() {
|
|
28
|
+
commander_1.program.version('0.0.1')
|
|
29
|
+
.name("lambda-router")
|
|
30
|
+
.description("Contextualize's solution for creating RESTful lambda applications");
|
|
31
|
+
commander_1.program.command("serve")
|
|
32
|
+
.description("Run lambda as a dedicated server")
|
|
33
|
+
.argument('<main>', 'Main file for starting the lambda')
|
|
34
|
+
.option('-p, --port <number>', 'server port', '8000')
|
|
35
|
+
.option('-e, --entry <string>', 'entry point function', 'lambdaHandler')
|
|
36
|
+
.action(async (entry, options) => {
|
|
37
|
+
const absoluteEntryPt = path.join(process.cwd(), entry);
|
|
38
|
+
console.log(absoluteEntryPt);
|
|
39
|
+
Object.defineProperty(global, "awslambda", {
|
|
40
|
+
value: {
|
|
41
|
+
streamifyResponse: (lambda) => {
|
|
42
|
+
_1.Server.serve((req, res) => {
|
|
43
|
+
const writable = new DummyWritable();
|
|
44
|
+
lambda(req, writable);
|
|
45
|
+
writable.result.then((value) => res(JSON.parse(value)));
|
|
46
|
+
}, {
|
|
47
|
+
port: parseInt(options.port)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const entryPointFile = require(absoluteEntryPt);
|
|
53
|
+
if (typeof entryPointFile[options.entry] === 'function') {
|
|
54
|
+
const handlerFunc = entryPointFile[options.entry];
|
|
55
|
+
_1.Server.serve(async (req, res) => {
|
|
56
|
+
const response = await handlerFunc(req);
|
|
57
|
+
res(response);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
await commander_1.program.parseAsync(process.argv);
|
|
62
|
+
}
|
|
63
|
+
main();
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare class PublicError extends Error {
|
|
2
|
+
code: number;
|
|
3
|
+
constructor(code: number | undefined, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class BadRequestError extends PublicError {
|
|
6
|
+
constructor(message?: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class NotFoundError extends PublicError {
|
|
9
|
+
constructor(message?: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class MethodNotAllowedError extends PublicError {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class NotImplementedError extends PublicError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class ConflictError extends PublicError {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class NotAuthenticatedError extends PublicError {
|
|
21
|
+
constructor(message?: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class PermissionDeniedError extends PublicError {
|
|
24
|
+
constructor(message?: string);
|
|
25
|
+
}
|
|
26
|
+
export declare class ExpectedBodyError extends BadRequestError {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
export declare class BodyValidationError extends BadRequestError {
|
|
30
|
+
missingField: string;
|
|
31
|
+
constructor(missingField: string);
|
|
32
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BodyValidationError = exports.ExpectedBodyError = exports.PermissionDeniedError = exports.NotAuthenticatedError = exports.ConflictError = exports.NotImplementedError = exports.MethodNotAllowedError = exports.NotFoundError = exports.BadRequestError = exports.PublicError = void 0;
|
|
4
|
+
// Base class for errors that are safe to show the end user
|
|
5
|
+
class PublicError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code = 500, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.PublicError = PublicError;
|
|
13
|
+
class BadRequestError extends PublicError {
|
|
14
|
+
constructor(message = "Bad Request") {
|
|
15
|
+
super(400, message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.BadRequestError = BadRequestError;
|
|
19
|
+
class NotFoundError extends PublicError {
|
|
20
|
+
constructor(message = "Not Found") {
|
|
21
|
+
super(404, message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.NotFoundError = NotFoundError;
|
|
25
|
+
class MethodNotAllowedError extends PublicError {
|
|
26
|
+
constructor(message = "Method Not Allowed") {
|
|
27
|
+
super(405, message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.MethodNotAllowedError = MethodNotAllowedError;
|
|
31
|
+
class NotImplementedError extends PublicError {
|
|
32
|
+
constructor(message = "Not Implemented") {
|
|
33
|
+
super(501, message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.NotImplementedError = NotImplementedError;
|
|
37
|
+
class ConflictError extends PublicError {
|
|
38
|
+
constructor(message = "Conflict Error") {
|
|
39
|
+
super(409, message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.ConflictError = ConflictError;
|
|
43
|
+
class NotAuthenticatedError extends PublicError {
|
|
44
|
+
constructor(message = "Not Authenticated") {
|
|
45
|
+
super(401, message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.NotAuthenticatedError = NotAuthenticatedError;
|
|
49
|
+
class PermissionDeniedError extends PublicError {
|
|
50
|
+
constructor(message = "Permission Denied") {
|
|
51
|
+
super(403, message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.PermissionDeniedError = PermissionDeniedError;
|
|
55
|
+
class ExpectedBodyError extends BadRequestError {
|
|
56
|
+
constructor() {
|
|
57
|
+
super("No body included in request");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.ExpectedBodyError = ExpectedBodyError;
|
|
61
|
+
class BodyValidationError extends BadRequestError {
|
|
62
|
+
missingField;
|
|
63
|
+
constructor(missingField) {
|
|
64
|
+
super(`Required field '${missingField}' missing`);
|
|
65
|
+
this.missingField = missingField;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.BodyValidationError = BodyValidationError;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { APIGatewayEvent } from "aws-lambda";
|
|
2
|
+
import Router from "./router";
|
|
3
|
+
import APIGatewayResponse from "./response";
|
|
4
|
+
import Server from "./server";
|
|
5
|
+
import { MinimumAPIGatewayEvent } from "./request";
|
|
6
|
+
export type HandlerSetup = (event: MinimumAPIGatewayEvent) => (Router | Promise<Router>);
|
|
7
|
+
export type HandlerCleanup = (event: MinimumAPIGatewayEvent) => void | Promise<void>;
|
|
8
|
+
export type BackgroundProcess = Promise<any | void>;
|
|
9
|
+
export default class Handler {
|
|
10
|
+
private isInHandler;
|
|
11
|
+
private canDoBackground;
|
|
12
|
+
private backgroundProcesses;
|
|
13
|
+
private static executeRouter;
|
|
14
|
+
static handler(setup: HandlerSetup, cleanup?: HandlerCleanup): (event: APIGatewayEvent) => Promise<APIGatewayResponse<any>>;
|
|
15
|
+
background(process: BackgroundProcess | (() => BackgroundProcess)): void;
|
|
16
|
+
waitForBackgroundProcesses(): Promise<void>;
|
|
17
|
+
static serverHandler(setup: HandlerSetup, cleanup?: HandlerCleanup, props?: Parameters<typeof Server.serve>[1]): Promise<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse> | import("https").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>>;
|
|
18
|
+
static backgroundHandler(setup: HandlerSetup, cleanup?: HandlerCleanup): import("aws-lambda").Handler<any, any>;
|
|
19
|
+
}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const response_1 = require("./response");
|
|
4
|
+
const server_1 = require("./server");
|
|
5
|
+
class Handler {
|
|
6
|
+
isInHandler = false;
|
|
7
|
+
canDoBackground = false;
|
|
8
|
+
backgroundProcesses = [];
|
|
9
|
+
static async executeRouter(event, handler, setup) {
|
|
10
|
+
handler.isInHandler = true;
|
|
11
|
+
let result;
|
|
12
|
+
try {
|
|
13
|
+
const router = await setup(event);
|
|
14
|
+
result = await router.process(event, handler);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error(e);
|
|
18
|
+
result = new response_1.default();
|
|
19
|
+
result.status(500).json({ message: "An unhandled error was encountered" });
|
|
20
|
+
}
|
|
21
|
+
handler.isInHandler = false;
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
static handler(setup, cleanup) {
|
|
25
|
+
const handler = new Handler();
|
|
26
|
+
handler.canDoBackground = false;
|
|
27
|
+
return async (event) => {
|
|
28
|
+
const result = await this.executeRouter(event, handler, setup);
|
|
29
|
+
if (cleanup) {
|
|
30
|
+
await cleanup(event);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
background(process) {
|
|
36
|
+
console.log("Registering background process");
|
|
37
|
+
if (!this.canDoBackground) {
|
|
38
|
+
throw new Error("Lambda handler not configured to use background processes");
|
|
39
|
+
}
|
|
40
|
+
if (!this.isInHandler) {
|
|
41
|
+
throw new Error("Cannot push a new background process outside of router execution");
|
|
42
|
+
}
|
|
43
|
+
if (typeof process === 'function') {
|
|
44
|
+
process = process();
|
|
45
|
+
}
|
|
46
|
+
this.backgroundProcesses.push(process);
|
|
47
|
+
}
|
|
48
|
+
async waitForBackgroundProcesses() {
|
|
49
|
+
this.canDoBackground = false;
|
|
50
|
+
if (this.backgroundProcesses.length > 0) {
|
|
51
|
+
console.log("Awaiting background processes");
|
|
52
|
+
const results = await Promise.allSettled(this.backgroundProcesses);
|
|
53
|
+
results.forEach(result => {
|
|
54
|
+
if (result.status === 'rejected') {
|
|
55
|
+
console.error(`Background process failed with error:`);
|
|
56
|
+
console.error(result.reason);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
static serverHandler(setup, cleanup, props) {
|
|
62
|
+
const handler = new Handler();
|
|
63
|
+
handler.canDoBackground = true;
|
|
64
|
+
return server_1.default.serve((async (event, response) => {
|
|
65
|
+
let result = await this.executeRouter(event, handler, setup);
|
|
66
|
+
response(result);
|
|
67
|
+
await handler.waitForBackgroundProcesses();
|
|
68
|
+
if (cleanup) {
|
|
69
|
+
await cleanup(event);
|
|
70
|
+
}
|
|
71
|
+
}), props);
|
|
72
|
+
}
|
|
73
|
+
static backgroundHandler(setup, cleanup) {
|
|
74
|
+
return awslambda.streamifyResponse(async (event, responseStream, _context) => {
|
|
75
|
+
const handler = new Handler();
|
|
76
|
+
handler.canDoBackground = true;
|
|
77
|
+
let result = await this.executeRouter(event, handler, setup);
|
|
78
|
+
responseStream.write(JSON.stringify(result));
|
|
79
|
+
responseStream.end();
|
|
80
|
+
await handler.waitForBackgroundProcesses();
|
|
81
|
+
if (cleanup) {
|
|
82
|
+
await cleanup(event);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.default = Handler;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MaybePromise } from ".";
|
|
2
|
+
export interface DependancyHealthcheckStatus {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
status?: any;
|
|
5
|
+
}
|
|
6
|
+
export interface Dependancy {
|
|
7
|
+
name: string;
|
|
8
|
+
optional?: boolean;
|
|
9
|
+
check: () => MaybePromise<DependancyHealthcheckStatus>;
|
|
10
|
+
}
|
|
11
|
+
export interface DependancyStatus extends DependancyHealthcheckStatus {
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export interface HealthcheckStatus {
|
|
15
|
+
live: boolean;
|
|
16
|
+
dependancies: DependancyStatus[];
|
|
17
|
+
serverTime: Date;
|
|
18
|
+
}
|
|
19
|
+
export default function Healthcheck(dependancies: Dependancy[]): Promise<HealthcheckStatus>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = Healthcheck;
|
|
4
|
+
async function Healthcheck(dependancies) {
|
|
5
|
+
let live = true;
|
|
6
|
+
const checkingDependancies = dependancies.map(async (dependancy) => {
|
|
7
|
+
try {
|
|
8
|
+
return await dependancy.check();
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
return { ok: false };
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const statuses = (await Promise.allSettled(checkingDependancies)).map((status, i) => {
|
|
15
|
+
const srcDependancy = dependancies[i];
|
|
16
|
+
if (status.status === 'rejected') {
|
|
17
|
+
return {
|
|
18
|
+
name: srcDependancy.name,
|
|
19
|
+
ok: false,
|
|
20
|
+
status: status.reason
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (!status.value.ok && !srcDependancy.optional) {
|
|
25
|
+
live = false;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
name: srcDependancy.name,
|
|
29
|
+
...status.value
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
live,
|
|
35
|
+
dependancies: statuses,
|
|
36
|
+
serverTime: new Date()
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Router from './router';
|
|
2
|
+
import Handler from './handler';
|
|
3
|
+
import * as Errors from './errors';
|
|
4
|
+
import APIGatewayRequest from './request';
|
|
5
|
+
import APIGatewayResponse from './response';
|
|
6
|
+
import Server from './server';
|
|
7
|
+
import Healthcheck from './healthcheck';
|
|
8
|
+
export type MaybePromise<T> = Promise<T> | T;
|
|
9
|
+
export { Router, Errors, Handler, APIGatewayRequest, APIGatewayResponse, Server, Healthcheck };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Healthcheck = exports.Server = exports.APIGatewayResponse = exports.APIGatewayRequest = exports.Handler = exports.Errors = exports.Router = void 0;
|
|
4
|
+
const router_1 = require("./router");
|
|
5
|
+
exports.Router = router_1.default;
|
|
6
|
+
const handler_1 = require("./handler");
|
|
7
|
+
exports.Handler = handler_1.default;
|
|
8
|
+
const Errors = require("./errors");
|
|
9
|
+
exports.Errors = Errors;
|
|
10
|
+
const request_1 = require("./request");
|
|
11
|
+
exports.APIGatewayRequest = request_1.default;
|
|
12
|
+
const response_1 = require("./response");
|
|
13
|
+
exports.APIGatewayResponse = response_1.default;
|
|
14
|
+
const server_1 = require("./server");
|
|
15
|
+
exports.Server = server_1.default;
|
|
16
|
+
const healthcheck_1 = require("./healthcheck");
|
|
17
|
+
exports.Healthcheck = healthcheck_1.default;
|
package/dist/path.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import APIGatewayRequest from "./request";
|
|
2
|
+
import Router, { Endpoint } from "./router";
|
|
3
|
+
interface MiddlewareWrapped<T> {
|
|
4
|
+
handler: T;
|
|
5
|
+
middlewareIndex: number;
|
|
6
|
+
}
|
|
7
|
+
export default class Path<T> {
|
|
8
|
+
router: Router<T>;
|
|
9
|
+
methods: Record<string, MiddlewareWrapped<Endpoint<T>>>;
|
|
10
|
+
subroute?: MiddlewareWrapped<Router<T & any>>;
|
|
11
|
+
children: Record<string, Path<T>>;
|
|
12
|
+
regexp: RegexPath<T>[];
|
|
13
|
+
constructor(router: Router<T>);
|
|
14
|
+
getEnd(path: string[], index: number): Path<T>;
|
|
15
|
+
find(request: APIGatewayRequest<T>, path: string[], index: number, routers: {
|
|
16
|
+
router: Router<T & any>;
|
|
17
|
+
index: number;
|
|
18
|
+
}[]): Endpoint<T>;
|
|
19
|
+
}
|
|
20
|
+
export declare class RegexPath<T> extends Path<T> {
|
|
21
|
+
key: string;
|
|
22
|
+
regex: string;
|
|
23
|
+
constructor(router: Router<T>, key: string);
|
|
24
|
+
matches(pathSegment: string): boolean;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
package/dist/path.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RegexPath = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
class Path {
|
|
6
|
+
router;
|
|
7
|
+
methods = {};
|
|
8
|
+
subroute;
|
|
9
|
+
children = {};
|
|
10
|
+
regexp = [];
|
|
11
|
+
constructor(router) {
|
|
12
|
+
this.router = router;
|
|
13
|
+
}
|
|
14
|
+
getEnd(path, index) {
|
|
15
|
+
if (path.length === index) {
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
const currentPath = path[index];
|
|
19
|
+
let reg;
|
|
20
|
+
if (reg = /^\{(.*)\}$/.exec(currentPath)) {
|
|
21
|
+
const key = reg[1];
|
|
22
|
+
let regHandler = this.regexp.find((regex) => {
|
|
23
|
+
return regex.key === key;
|
|
24
|
+
});
|
|
25
|
+
if (!regHandler) {
|
|
26
|
+
regHandler = new RegexPath(this.router, key);
|
|
27
|
+
this.regexp.push(regHandler);
|
|
28
|
+
}
|
|
29
|
+
return regHandler.getEnd(path, index + 1);
|
|
30
|
+
}
|
|
31
|
+
if (!(currentPath in this.children)) {
|
|
32
|
+
this.children[currentPath] = new Path(this.router);
|
|
33
|
+
}
|
|
34
|
+
return this.children[currentPath].getEnd(path, index + 1);
|
|
35
|
+
}
|
|
36
|
+
find(request, path, index, routers) {
|
|
37
|
+
if (path.length === index) {
|
|
38
|
+
let methodHandler;
|
|
39
|
+
if (!(request.method in this.methods)) {
|
|
40
|
+
if (!('ANY' in this.methods)) {
|
|
41
|
+
if (this.subroute) {
|
|
42
|
+
const result = this.subroute.handler.basePath.find(request, path, index, routers);
|
|
43
|
+
routers.push({ router: this.router, index: this.subroute.middlewareIndex });
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(this.methods).length === 0) {
|
|
47
|
+
throw new errors_1.NotFoundError();
|
|
48
|
+
}
|
|
49
|
+
throw new errors_1.MethodNotAllowedError();
|
|
50
|
+
}
|
|
51
|
+
methodHandler = this.methods['ANY'];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
methodHandler = this.methods[request.method];
|
|
55
|
+
}
|
|
56
|
+
routers.push({ router: this.router, index: methodHandler.middlewareIndex });
|
|
57
|
+
return methodHandler.handler;
|
|
58
|
+
}
|
|
59
|
+
const currentPath = path[index];
|
|
60
|
+
if (currentPath in this.children) {
|
|
61
|
+
const inPath = this.children[currentPath];
|
|
62
|
+
try {
|
|
63
|
+
const result = inPath.find(request, path, index + 1, routers);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
if (e instanceof errors_1.MethodNotAllowedError) {
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (this.subroute) {
|
|
73
|
+
const inPath = this.subroute;
|
|
74
|
+
try {
|
|
75
|
+
const result = inPath.handler.basePath.find(request, path, index, routers);
|
|
76
|
+
routers.push({ router: this.router, index: inPath.middlewareIndex });
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (e instanceof errors_1.MethodNotAllowedError) {
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const regex of this.regexp) {
|
|
86
|
+
if (regex.matches(currentPath)) {
|
|
87
|
+
request.params[regex.key] = currentPath;
|
|
88
|
+
const result = regex.find(request, path, index + 1, routers);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new errors_1.NotFoundError();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.default = Path;
|
|
96
|
+
class RegexPath extends Path {
|
|
97
|
+
key;
|
|
98
|
+
regex;
|
|
99
|
+
constructor(router, key) {
|
|
100
|
+
super(router);
|
|
101
|
+
this.key = key;
|
|
102
|
+
}
|
|
103
|
+
matches(pathSegment) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.RegexPath = RegexPath;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { APIGatewayEvent } from "aws-lambda";
|
|
2
|
+
import Handler from "./handler";
|
|
3
|
+
export declare enum CommonHeaders {
|
|
4
|
+
ContentLength = "Content-Length",
|
|
5
|
+
ContentType = "Content-Type",
|
|
6
|
+
Authorization = "Authorization"
|
|
7
|
+
}
|
|
8
|
+
export type MinimumAPIGatewayEvent = Pick<APIGatewayEvent, 'headers' | 'path' | 'httpMethod' | 'queryStringParameters' | 'body'>;
|
|
9
|
+
export default class APIGatewayRequest<Application = {}> {
|
|
10
|
+
handler?: Handler | undefined;
|
|
11
|
+
headers: Record<CommonHeaders | string, any>;
|
|
12
|
+
path: string;
|
|
13
|
+
method: string;
|
|
14
|
+
query: Record<string, any>;
|
|
15
|
+
params: any;
|
|
16
|
+
body: any;
|
|
17
|
+
app: Application;
|
|
18
|
+
constructor(event: MinimumAPIGatewayEvent, handler?: Handler | undefined);
|
|
19
|
+
assertBody(...fields: string[]): boolean;
|
|
20
|
+
}
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommonHeaders = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
// {
|
|
6
|
+
// "body": "eyJ0ZXN0IjoiYm9keSJ9",
|
|
7
|
+
// "resource": "/{proxy+}",
|
|
8
|
+
// "path": "/",
|
|
9
|
+
// "httpMethod": "GET",
|
|
10
|
+
// "isBase64Encoded": true,
|
|
11
|
+
// "queryStringParameters": {
|
|
12
|
+
// "foo": "bar"
|
|
13
|
+
// },
|
|
14
|
+
// "multiValueQueryStringParameters": {
|
|
15
|
+
// "foo": [
|
|
16
|
+
// "bar"
|
|
17
|
+
// ]
|
|
18
|
+
// },
|
|
19
|
+
// "pathParameters": {
|
|
20
|
+
// "proxy": "/path/to/resource"
|
|
21
|
+
// },
|
|
22
|
+
// "stageVariables": {
|
|
23
|
+
// "baz": "qux"
|
|
24
|
+
// },
|
|
25
|
+
// "headers": {
|
|
26
|
+
// "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
27
|
+
// "Accept-Encoding": "gzip, deflate, sdch",
|
|
28
|
+
// "Accept-Language": "en-US,en;q=0.8",
|
|
29
|
+
// "Cache-Control": "max-age=0",
|
|
30
|
+
// "CloudFront-Forwarded-Proto": "https",
|
|
31
|
+
// "CloudFront-Is-Desktop-Viewer": "true",
|
|
32
|
+
// "CloudFront-Is-Mobile-Viewer": "false",
|
|
33
|
+
// "CloudFront-Is-SmartTV-Viewer": "false",
|
|
34
|
+
// "CloudFront-Is-Tablet-Viewer": "false",
|
|
35
|
+
// "CloudFront-Viewer-Country": "US",
|
|
36
|
+
// "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
|
|
37
|
+
// "Upgrade-Insecure-Requests": "1",
|
|
38
|
+
// "User-Agent": "Custom User Agent String",
|
|
39
|
+
// "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
|
|
40
|
+
// "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
|
|
41
|
+
// "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
|
|
42
|
+
// "X-Forwarded-Port": "443",
|
|
43
|
+
// "X-Forwarded-Proto": "https"
|
|
44
|
+
// },
|
|
45
|
+
// "multiValueHeaders": {
|
|
46
|
+
// "Accept": [
|
|
47
|
+
// "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
|
|
48
|
+
// ],
|
|
49
|
+
// "Accept-Encoding": [
|
|
50
|
+
// "gzip, deflate, sdch"
|
|
51
|
+
// ],
|
|
52
|
+
// "Accept-Language": [
|
|
53
|
+
// "en-US,en;q=0.8"
|
|
54
|
+
// ],
|
|
55
|
+
// "Cache-Control": [
|
|
56
|
+
// "max-age=0"
|
|
57
|
+
// ],
|
|
58
|
+
// "CloudFront-Forwarded-Proto": [
|
|
59
|
+
// "https"
|
|
60
|
+
// ],
|
|
61
|
+
// "CloudFront-Is-Desktop-Viewer": [
|
|
62
|
+
// "true"
|
|
63
|
+
// ],
|
|
64
|
+
// "CloudFront-Is-Mobile-Viewer": [
|
|
65
|
+
// "false"
|
|
66
|
+
// ],
|
|
67
|
+
// "CloudFront-Is-SmartTV-Viewer": [
|
|
68
|
+
// "false"
|
|
69
|
+
// ],
|
|
70
|
+
// "CloudFront-Is-Tablet-Viewer": [
|
|
71
|
+
// "false"
|
|
72
|
+
// ],
|
|
73
|
+
// "CloudFront-Viewer-Country": [
|
|
74
|
+
// "US"
|
|
75
|
+
// ],
|
|
76
|
+
// "Host": [
|
|
77
|
+
// "0123456789.execute-api.us-east-1.amazonaws.com"
|
|
78
|
+
// ],
|
|
79
|
+
// "Upgrade-Insecure-Requests": [
|
|
80
|
+
// "1"
|
|
81
|
+
// ],
|
|
82
|
+
// "User-Agent": [
|
|
83
|
+
// "Custom User Agent String"
|
|
84
|
+
// ],
|
|
85
|
+
// "Via": [
|
|
86
|
+
// "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
|
|
87
|
+
// ],
|
|
88
|
+
// "X-Amz-Cf-Id": [
|
|
89
|
+
// "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
|
|
90
|
+
// ],
|
|
91
|
+
// "X-Forwarded-For": [
|
|
92
|
+
// "127.0.0.1, 127.0.0.2"
|
|
93
|
+
// ],
|
|
94
|
+
// "X-Forwarded-Port": [
|
|
95
|
+
// "443"
|
|
96
|
+
// ],
|
|
97
|
+
// "X-Forwarded-Proto": [
|
|
98
|
+
// "https"
|
|
99
|
+
// ]
|
|
100
|
+
// },
|
|
101
|
+
// "requestContext": {
|
|
102
|
+
// "accountId": "123456789012",
|
|
103
|
+
// "resourceId": "123456",
|
|
104
|
+
// "stage": "prod",
|
|
105
|
+
// "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
|
|
106
|
+
// "requestTime": "09/Apr/2015:12:34:56 +0000",
|
|
107
|
+
// "requestTimeEpoch": 1428582896000,
|
|
108
|
+
// "identity": {
|
|
109
|
+
// "cognitoIdentityPoolId": null,
|
|
110
|
+
// "accountId": null,
|
|
111
|
+
// "cognitoIdentityId": null,
|
|
112
|
+
// "caller": null,
|
|
113
|
+
// "accessKey": null,
|
|
114
|
+
// "sourceIp": "127.0.0.1",
|
|
115
|
+
// "cognitoAuthenticationType": null,
|
|
116
|
+
// "cognitoAuthenticationProvider": null,
|
|
117
|
+
// "userArn": null,
|
|
118
|
+
// "userAgent": "Custom User Agent String",
|
|
119
|
+
// "user": null
|
|
120
|
+
// },
|
|
121
|
+
// "path": "/prod/path/to/resource",
|
|
122
|
+
// "resourcePath": "/{proxy+}",
|
|
123
|
+
// "httpMethod": "POST",
|
|
124
|
+
// "apiId": "1234567890",
|
|
125
|
+
// "protocol": "HTTP/1.1"
|
|
126
|
+
// }
|
|
127
|
+
// }
|
|
128
|
+
var CommonHeaders;
|
|
129
|
+
(function (CommonHeaders) {
|
|
130
|
+
CommonHeaders["ContentLength"] = "Content-Length";
|
|
131
|
+
CommonHeaders["ContentType"] = "Content-Type";
|
|
132
|
+
CommonHeaders["Authorization"] = "Authorization";
|
|
133
|
+
})(CommonHeaders || (exports.CommonHeaders = CommonHeaders = {}));
|
|
134
|
+
class APIGatewayRequest {
|
|
135
|
+
handler;
|
|
136
|
+
headers;
|
|
137
|
+
;
|
|
138
|
+
path;
|
|
139
|
+
method;
|
|
140
|
+
query;
|
|
141
|
+
params;
|
|
142
|
+
body;
|
|
143
|
+
app;
|
|
144
|
+
constructor(event, handler) {
|
|
145
|
+
this.handler = handler;
|
|
146
|
+
this.headers = event.headers;
|
|
147
|
+
this.params = {};
|
|
148
|
+
this.path = event.path;
|
|
149
|
+
this.method = event.httpMethod;
|
|
150
|
+
this.query = event.queryStringParameters ?? {};
|
|
151
|
+
this.body = event.body;
|
|
152
|
+
if (CommonHeaders.ContentType in this.headers) {
|
|
153
|
+
switch (this.headers[CommonHeaders.ContentType]) {
|
|
154
|
+
case 'application/json':
|
|
155
|
+
this.body = event.body ? JSON.parse(event.body) : undefined;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
assertBody(...fields) {
|
|
161
|
+
if (!this.body) {
|
|
162
|
+
throw new errors_1.ExpectedBodyError();
|
|
163
|
+
}
|
|
164
|
+
for (const field of fields) {
|
|
165
|
+
if (!(field in this.body)) {
|
|
166
|
+
throw new errors_1.BodyValidationError(field);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.default = APIGatewayRequest;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare enum StatusCode {
|
|
2
|
+
ok = 200,
|
|
3
|
+
new = 201
|
|
4
|
+
}
|
|
5
|
+
export default class APIGatewayResponse<T = any> {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
body?: any;
|
|
9
|
+
constructor();
|
|
10
|
+
status(code: number): this;
|
|
11
|
+
json(body: T): this;
|
|
12
|
+
text(body: string): this;
|
|
13
|
+
}
|
package/dist/response.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StatusCode = void 0;
|
|
4
|
+
var StatusCode;
|
|
5
|
+
(function (StatusCode) {
|
|
6
|
+
StatusCode[StatusCode["ok"] = 200] = "ok";
|
|
7
|
+
StatusCode[StatusCode["new"] = 201] = "new";
|
|
8
|
+
})(StatusCode || (exports.StatusCode = StatusCode = {}));
|
|
9
|
+
class APIGatewayResponse {
|
|
10
|
+
statusCode;
|
|
11
|
+
headers;
|
|
12
|
+
;
|
|
13
|
+
body;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.statusCode = 200;
|
|
16
|
+
this.headers = {};
|
|
17
|
+
this.body = undefined;
|
|
18
|
+
}
|
|
19
|
+
status(code) {
|
|
20
|
+
this.statusCode = code;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
json(body) {
|
|
24
|
+
this.headers['Content-Type'] = "application/json";
|
|
25
|
+
this.body = JSON.stringify(body);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
text(body) {
|
|
29
|
+
this.headers['Content-Type'] = "text/plain";
|
|
30
|
+
this.body = body;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.default = APIGatewayResponse;
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Path from "./path";
|
|
2
|
+
import APIGatewayRequest, { MinimumAPIGatewayEvent } from "./request";
|
|
3
|
+
import APIGatewayResponse from "./response";
|
|
4
|
+
import Handler from "./handler";
|
|
5
|
+
export type Endpoint<App> = (request: APIGatewayRequest<App>, response: APIGatewayResponse) => void;
|
|
6
|
+
export type Middleware<T> = (request: APIGatewayRequest<T>) => void;
|
|
7
|
+
export default class Router<T = {}> {
|
|
8
|
+
basePath: Path<T>;
|
|
9
|
+
middleware: Middleware<T>[];
|
|
10
|
+
constructor();
|
|
11
|
+
get(path: string, callback: Endpoint<T>): void;
|
|
12
|
+
post(path: string, callback: Endpoint<T>): void;
|
|
13
|
+
put(path: string, callback: Endpoint<T>): void;
|
|
14
|
+
delete(path: string, callback: Endpoint<T>): void;
|
|
15
|
+
any(path: string, callback: Endpoint<T>): void;
|
|
16
|
+
route(method: string, endpoint: string, handler: Endpoint<T>): void;
|
|
17
|
+
use(middleware: Middleware<T>): void;
|
|
18
|
+
subroute<Y extends T>(endpoint: string, router: Router<Y>): void;
|
|
19
|
+
process(event: MinimumAPIGatewayEvent, handler?: Handler): Promise<APIGatewayResponse<any>>;
|
|
20
|
+
}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path_1 = require("./path");
|
|
4
|
+
const request_1 = require("./request");
|
|
5
|
+
const response_1 = require("./response");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
class Router {
|
|
8
|
+
basePath;
|
|
9
|
+
middleware;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.basePath = new path_1.default(this);
|
|
12
|
+
this.middleware = [];
|
|
13
|
+
}
|
|
14
|
+
get(path, callback) {
|
|
15
|
+
this.route('GET', path, callback);
|
|
16
|
+
}
|
|
17
|
+
post(path, callback) {
|
|
18
|
+
this.route('POST', path, callback);
|
|
19
|
+
}
|
|
20
|
+
put(path, callback) {
|
|
21
|
+
this.route('PUT', path, callback);
|
|
22
|
+
}
|
|
23
|
+
delete(path, callback) {
|
|
24
|
+
this.route('DELETE', path, callback);
|
|
25
|
+
}
|
|
26
|
+
any(path, callback) {
|
|
27
|
+
this.route('ANY', path, callback);
|
|
28
|
+
}
|
|
29
|
+
route(method, endpoint, handler) {
|
|
30
|
+
const path = endpoint.split('/').filter(a => !!a);
|
|
31
|
+
const end = this.basePath.getEnd(path, 0);
|
|
32
|
+
end.methods[method] = { handler, middlewareIndex: this.middleware.length };
|
|
33
|
+
}
|
|
34
|
+
use(middleware) {
|
|
35
|
+
this.middleware.push(middleware);
|
|
36
|
+
}
|
|
37
|
+
subroute(endpoint, router) {
|
|
38
|
+
const path = endpoint.split('/').filter(a => !!a);
|
|
39
|
+
const end = this.basePath.getEnd(path, 0);
|
|
40
|
+
end.subroute = { handler: router, middlewareIndex: this.middleware.length };
|
|
41
|
+
}
|
|
42
|
+
async process(event, handler) {
|
|
43
|
+
const response = new response_1.default();
|
|
44
|
+
const request = new request_1.default(event, handler);
|
|
45
|
+
console.log(`${request.method.padEnd(6, ' ')} ${request.path}`);
|
|
46
|
+
const path = request.path.split('/').filter(a => !!a);
|
|
47
|
+
try {
|
|
48
|
+
const routerPath = [];
|
|
49
|
+
const foundEndpoint = this.basePath.find(request, path, 0, routerPath);
|
|
50
|
+
if (!foundEndpoint) {
|
|
51
|
+
throw new errors_1.NotFoundError();
|
|
52
|
+
}
|
|
53
|
+
routerPath.reverse();
|
|
54
|
+
for (const { router, index } of routerPath) {
|
|
55
|
+
const usedMiddleware = router.middleware.slice(0, index);
|
|
56
|
+
for (const middlware of usedMiddleware) {
|
|
57
|
+
await middlware(request);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await foundEndpoint(request, response);
|
|
61
|
+
}
|
|
62
|
+
catch (exception) {
|
|
63
|
+
if (exception instanceof errors_1.PublicError) {
|
|
64
|
+
console.warn(`Request failed gracefully with code: ${exception.code} ${exception.message}`);
|
|
65
|
+
response.statusCode = exception.code;
|
|
66
|
+
response.json({ message: exception.message });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.error(`Request experienced a critical error`);
|
|
70
|
+
throw exception;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.default = Router;
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as https from 'https';
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
import { MinimumAPIGatewayEvent } from "./request";
|
|
4
|
+
import APIGatewayResponse from "./response";
|
|
5
|
+
import { MaybePromise } from ".";
|
|
6
|
+
interface ServerProps {
|
|
7
|
+
port?: number;
|
|
8
|
+
protocol?: 'http' | 'https';
|
|
9
|
+
}
|
|
10
|
+
export default class Server {
|
|
11
|
+
private static normalizeProps;
|
|
12
|
+
private static convertIncomingMessage;
|
|
13
|
+
static processResponse(response: http.ServerResponse): (routerResponse: APIGatewayResponse) => Promise<void>;
|
|
14
|
+
static serve(handler: (request: MinimumAPIGatewayEvent, response: (response: APIGatewayResponse) => void) => MaybePromise<void>, props?: ServerProps): Promise<http.Server | https.Server>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
class Server {
|
|
6
|
+
static normalizeProps(props) {
|
|
7
|
+
const defaults = {
|
|
8
|
+
port: 8000,
|
|
9
|
+
protocol: 'http'
|
|
10
|
+
};
|
|
11
|
+
if (typeof props === 'undefined') {
|
|
12
|
+
return defaults;
|
|
13
|
+
}
|
|
14
|
+
return { ...defaults, ...props };
|
|
15
|
+
}
|
|
16
|
+
static async convertIncomingMessage(message) {
|
|
17
|
+
const headers = {};
|
|
18
|
+
//_mvHeaders: Record<string, string[]> = {};
|
|
19
|
+
Object.entries(message.headers).forEach(([k, v]) => {
|
|
20
|
+
if (!v) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(v)) {
|
|
24
|
+
//_mvHeaders[k] = v;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const parts = k.split('-');
|
|
28
|
+
for (let i = 0; i < parts.length; i++) {
|
|
29
|
+
if (parts[i]) {
|
|
30
|
+
parts[i] = `${parts[i][0].toUpperCase()}${parts[i].slice(1)}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
headers[parts.join('-')] = v;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
let body = null;
|
|
37
|
+
if (parseInt(message.headers["content-length"] ?? '0') !== 0) {
|
|
38
|
+
body = await (new Promise(resolve => {
|
|
39
|
+
const aggrigator = [];
|
|
40
|
+
message.on('data', (chunk) => {
|
|
41
|
+
aggrigator.push(chunk.toString());
|
|
42
|
+
});
|
|
43
|
+
message.on('end', () => {
|
|
44
|
+
resolve(aggrigator.join(''));
|
|
45
|
+
});
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
const url = new URL(`http://localhost${message.url ?? '/'}`);
|
|
49
|
+
return {
|
|
50
|
+
body: body,
|
|
51
|
+
headers: headers,
|
|
52
|
+
httpMethod: message.method ?? "GET",
|
|
53
|
+
path: url.pathname,
|
|
54
|
+
queryStringParameters: Object.fromEntries(url.searchParams)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
static processResponse(response) {
|
|
58
|
+
return async (routerResponse) => {
|
|
59
|
+
Object.entries(routerResponse.headers).forEach(([header, value]) => {
|
|
60
|
+
response.setHeader(header, value);
|
|
61
|
+
});
|
|
62
|
+
response.statusCode = routerResponse.statusCode;
|
|
63
|
+
if (routerResponse.body) {
|
|
64
|
+
await new Promise(resolve => {
|
|
65
|
+
response.write(routerResponse.body, resolve);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
response.end();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
static serve(handler, props) {
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
const normalProps = this.normalizeProps(props);
|
|
74
|
+
const serverHandler = normalProps.protocol === 'https' ? https.createServer : http.createServer;
|
|
75
|
+
const server = serverHandler({}, async (request, response) => {
|
|
76
|
+
const GatewayLikeRequest = await this.convertIncomingMessage(request);
|
|
77
|
+
handler(GatewayLikeRequest, this.processResponse(response));
|
|
78
|
+
});
|
|
79
|
+
server.listen(normalProps?.port, () => {
|
|
80
|
+
console.log(`${normalProps?.protocol} server listening on port ${normalProps.port}`);
|
|
81
|
+
resolve(server);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.default = Server;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contextualize/lambda-router",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Allows for the creation of Express-like APIs within AWS Lambda",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lambda-router": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc -w",
|
|
13
|
+
"test": "jest"
|
|
14
|
+
},
|
|
15
|
+
"author": "Contextualize LLC",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"files": [
|
|
18
|
+
"/dist"
|
|
19
|
+
],
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@jest/globals": "^29.7.0",
|
|
22
|
+
"@types/jest": "^29.5.14",
|
|
23
|
+
"@types/node": "^22.9.1",
|
|
24
|
+
"jest": "^29.7.0",
|
|
25
|
+
"ts-jest": "^29.2.5",
|
|
26
|
+
"typescript": "^5.6.3"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@types/aws-lambda": "^8.10.136",
|
|
30
|
+
"aws-lambda": "^1.0.7",
|
|
31
|
+
"aws-sdk": "^2.1581.0",
|
|
32
|
+
"commander": "^14.0.0"
|
|
33
|
+
},
|
|
34
|
+
"browser": {
|
|
35
|
+
"crypto": false
|
|
36
|
+
}
|
|
37
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Lambda Router
|
|
2
|
+
|
|
3
|
+
Allows for the creation of Express-like APIs within AWS Lambda.
|
|
4
|
+
|
|
5
|
+
## Create Router
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Router, Handler } from '@contextualize/lambda-router';
|
|
9
|
+
|
|
10
|
+
const router = new Router();
|
|
11
|
+
|
|
12
|
+
router.get('/', (req, res)=>{
|
|
13
|
+
return res.status(200).json({
|
|
14
|
+
status: "ok"
|
|
15
|
+
});
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const lambdaHandler = Handler.handler(()=>router);
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Background Handler
|
|
23
|
+
Some lambdas may require tasks to run in the background after a request has been handled. `Handler.backgroundHandler` allows you to defer promises for after the lambda has returned a response to the client.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { Router, Handler } from '@contextualize/lambda-router';
|
|
27
|
+
|
|
28
|
+
const router = new Router();
|
|
29
|
+
|
|
30
|
+
router.get('/', (req, res)=>{
|
|
31
|
+
req.handler.background(async ()=>{
|
|
32
|
+
|
|
33
|
+
// Assume this a request that is going to take a long time
|
|
34
|
+
await fetch('https://example.com')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return res.status(200).json({
|
|
38
|
+
status: "ok"
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const lambdaHandler = Handler.backgroundHandler(()=>router);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Run as Dedicated Server
|
|
46
|
+
You can run lambdas set up with lambda-router, and some that aren't using `lambda-router serve`. This will create a node native http server that converts HTTP requests into APIGateway-like requests, then converts the response from your router into an HTTP response.
|
|
47
|
+
|
|
48
|
+
This can be used for either local testing, or converting your serverless application to a server-based app should the needs of your application no-longer be found with lambda.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
lambda-router serve -h
|
|
52
|
+
Usage: lambda-router serve [options] <main>
|
|
53
|
+
|
|
54
|
+
Run lambda as a dedicated server
|
|
55
|
+
|
|
56
|
+
Arguments:
|
|
57
|
+
main Main file for starting the lambda
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
-p, --port <number> server port (default: "8000")
|
|
61
|
+
-e, --entry <string> entry point function (default: "lambdaHandler")
|
|
62
|
+
-h, --help display help for command
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
|