lambder 1.0.0
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/.eslintrc.cjs +26 -0
- package/.vscode/settings.json +15 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +196 -0
- package/package.json +30 -0
- package/src/Resolver.ts +198 -0
- package/src/index.ts +216 -0
- package/tsconfig.json +24 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"env": {
|
|
3
|
+
"browser": true,
|
|
4
|
+
"es2021": true
|
|
5
|
+
},
|
|
6
|
+
"extends": "standard-with-typescript",
|
|
7
|
+
"overrides": [
|
|
8
|
+
{
|
|
9
|
+
"env": {
|
|
10
|
+
"node": true
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
".eslintrc.{js,cjs}"
|
|
14
|
+
],
|
|
15
|
+
"parserOptions": {
|
|
16
|
+
"sourceType": "script"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"parserOptions": {
|
|
21
|
+
"ecmaVersion": "latest",
|
|
22
|
+
"sourceType": "module"
|
|
23
|
+
},
|
|
24
|
+
"rules": {
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
|
|
2
|
+
export declare const resolveEvent: (event: APIGatewayProxyEvent, resolve: any, reject: any) => {
|
|
3
|
+
host: string;
|
|
4
|
+
realHost: string;
|
|
5
|
+
url: string;
|
|
6
|
+
method: string;
|
|
7
|
+
get: {
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
};
|
|
10
|
+
post: {
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
cookie: any;
|
|
14
|
+
headers: import("aws-lambda").APIGatewayProxyEventHeaders;
|
|
15
|
+
};
|
|
16
|
+
declare const hookTypeList: readonly ["beforeRender", "afterRender", "created", "completed", "fallback"];
|
|
17
|
+
type HookType = typeof hookTypeList[number];
|
|
18
|
+
export default class Lambder {
|
|
19
|
+
apiPath: string;
|
|
20
|
+
corsEnabled: boolean;
|
|
21
|
+
private _actionList;
|
|
22
|
+
private _hookList;
|
|
23
|
+
constructor({ apiPath, corsEnabled }: {
|
|
24
|
+
apiPath?: string;
|
|
25
|
+
corsEnabled?: boolean;
|
|
26
|
+
});
|
|
27
|
+
private apiHandler;
|
|
28
|
+
private pathHandler;
|
|
29
|
+
private fallbackHandler;
|
|
30
|
+
addApi(condition: string | Function | RegExp, actionFn: Function): number;
|
|
31
|
+
addPath(condition: string | Function | RegExp, actionFn: Function): number;
|
|
32
|
+
addHook(when: HookType, hookFn: Function, priority?: number): Promise<void>;
|
|
33
|
+
addModule(moduleFn: any): Promise<void>;
|
|
34
|
+
render(event: APIGatewayProxyEvent, runContext: Context): Promise<unknown>;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import * as pathLibrary from "path";
|
|
3
|
+
import querystring from "querystring";
|
|
4
|
+
import cookieParser from "cookie";
|
|
5
|
+
const readFileSync = (filePath) => {
|
|
6
|
+
const __dirname = pathLibrary.resolve(pathLibrary.dirname(""));
|
|
7
|
+
const absolutePath = pathLibrary.resolve(filePath);
|
|
8
|
+
const publicPath = pathLibrary.resolve(`${__dirname}/www/`);
|
|
9
|
+
if (!absolutePath.includes(publicPath)) {
|
|
10
|
+
return "forbiddenpath";
|
|
11
|
+
}
|
|
12
|
+
return fs.readFileSync(filePath);
|
|
13
|
+
};
|
|
14
|
+
// export const millisecondCounter = (startTime = 0) => {
|
|
15
|
+
// const time = process.hrtime();
|
|
16
|
+
// return Number((time[0] + (time[1] / 1e9) - startTime).toFixed(3));
|
|
17
|
+
// };
|
|
18
|
+
export const resolveEvent = (event, resolve, reject) => {
|
|
19
|
+
const realHost = event.headers.Host || event.headers.host || "";
|
|
20
|
+
const host = realHost.replace(/^(\S+)--([a-z]+)\.beta00\.com$/, "$1.$2.com").replace(".beta00.com", ".com");
|
|
21
|
+
const url = event.path;
|
|
22
|
+
const get = event.queryStringParameters || {};
|
|
23
|
+
const method = event.httpMethod;
|
|
24
|
+
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
25
|
+
const headers = event.headers;
|
|
26
|
+
let post;
|
|
27
|
+
try {
|
|
28
|
+
const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
|
|
29
|
+
try {
|
|
30
|
+
post = JSON.parse(decodedBody) || {};
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
post = querystring.parse(decodedBody) || {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
post = {};
|
|
38
|
+
}
|
|
39
|
+
if (/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)) {
|
|
40
|
+
reject("[404] Host cannot be an ip address: " + host);
|
|
41
|
+
}
|
|
42
|
+
return { host, realHost, url, method, get, post, cookie, headers };
|
|
43
|
+
};
|
|
44
|
+
// const parseHeader = (
|
|
45
|
+
// headers: { [key:string]: string|string[] } | undefined
|
|
46
|
+
// ):{ [key:string]: string[] } =>
|
|
47
|
+
// Object.fromEntries(Object.entries(headers||{}).map(([k,v])=>[ k, Array.isArray(v) ? v : [v] ]));
|
|
48
|
+
// const CORS_HEADERS = parseHeader({
|
|
49
|
+
// "Access-Control-Allow-Credentials": "true",
|
|
50
|
+
// "Access-Control-Allow-Headers" : "Origin, X-Requested-With, Content-Type, Accept",
|
|
51
|
+
// "Access-Control-Allow-Origin": "*",
|
|
52
|
+
// "Access-Control-Allow-Methods": "OPTIONS,POST",
|
|
53
|
+
// });
|
|
54
|
+
// export const dieRaw = (param: { body: null|string, statusCode: number, multiValueHeaders: {[key:string]: string[] }, isBase64Encoded?: boolean }) => { console.log("HandlerEnd: " + millisecondCounter() + " Size: " + (param.body || {}).length ); return param; };
|
|
55
|
+
// export const dieJson = (data: { [key:string]: any }|string, headers?: { [key:string]: string|string[] }) => dieRaw({statusCode: 200,multiValueHeaders: { "Content-Type": ["application/json; charset=utf-8"], ...CORS_HEADERS, ...parseHeader(headers)},body: JSON.stringify(data),});
|
|
56
|
+
// export const dieXml = (data: string) => dieRaw({statusCode: 200,isBase64Encoded: true,multiValueHeaders: { "Content-Type": ["application/xml; charset=utf-8"]}, body: Buffer.from(data).toString("base64"),});
|
|
57
|
+
// export const dieHtml = (data: string, headers?: { [key:string]: string|string[] }) => dieRaw({statusCode: 200,isBase64Encoded: true,multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...parseHeader(headers)},body: Buffer.from(data).toString("base64"),});
|
|
58
|
+
// export const die301 = (url: string, headers?: { [key:string]: string|string[] }) => dieRaw({statusCode: 301, multiValueHeaders: { "Location" : [url], ...parseHeader(headers) },body:null});
|
|
59
|
+
// export const die404 = (data: string,headers?: { [key:string]: string|string[] }) => dieRaw({statusCode: 404,isBase64Encoded: true,multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...parseHeader(headers)},body: Buffer.from(data).toString("base64"),});
|
|
60
|
+
// export const dieCors = () => dieRaw({ statusCode: 200,multiValueHeaders: CORS_HEADERS,body: JSON.stringify(""),});
|
|
61
|
+
// export const dieFileBase64 = async (fileBase64: string, mimeType: string, headers?: { [key:string]: string|string[] }) => dieRaw({ statusCode: 200, isBase64Encoded: true, multiValueHeaders: { "Content-Type": [mimeType || "text/html"], ...parseHeader(headers) }, body: fileBase64, });
|
|
62
|
+
// export const dieFile = async (file: string, headers?: { [key:string]: string|string[] }) => {
|
|
63
|
+
// if(!fs.existsSync(file)) return dieJson("File not found: " + file );
|
|
64
|
+
// const mimeType = mimeTypeResolver.lookup(file);
|
|
65
|
+
// const body = readFileSync(file);
|
|
66
|
+
// const bodyBase64 = Buffer.from(body).toString("base64");
|
|
67
|
+
// console.log("bodyBase64.length",bodyBase64.length);
|
|
68
|
+
// return dieFileBase64(bodyBase64, mimeType || "", headers);
|
|
69
|
+
// };
|
|
70
|
+
// export const dieApi = ({
|
|
71
|
+
// payload, error=null,
|
|
72
|
+
// ...other
|
|
73
|
+
// }: {
|
|
74
|
+
// payload?: any;
|
|
75
|
+
// error?: any;
|
|
76
|
+
// }, headers?: { [key:string]: string|string[] })=>{
|
|
77
|
+
// if(error) console.log("ERROR: ", error);
|
|
78
|
+
// if(Object.keys(other).length) console.log("ERROR: Unexpected key in result: " + Object.keys(other).join(", "));
|
|
79
|
+
// return dieJson({
|
|
80
|
+
// version: lambdaConfig.version,
|
|
81
|
+
// payload,
|
|
82
|
+
// ...(error ? {error} : {}),
|
|
83
|
+
// },headers);
|
|
84
|
+
// }
|
|
85
|
+
const hookTypeList = ["beforeRender", "afterRender", "created", "completed", "fallback"];
|
|
86
|
+
export default class Lambder {
|
|
87
|
+
apiPath;
|
|
88
|
+
corsEnabled;
|
|
89
|
+
_actionList;
|
|
90
|
+
_hookList;
|
|
91
|
+
constructor({ apiPath, corsEnabled }) {
|
|
92
|
+
this.apiPath = apiPath ?? "/api";
|
|
93
|
+
this.corsEnabled = corsEnabled ?? true;
|
|
94
|
+
this._actionList = [];
|
|
95
|
+
this._hookList = {
|
|
96
|
+
"beforeRender": [],
|
|
97
|
+
"afterRender": [],
|
|
98
|
+
"fallback": [],
|
|
99
|
+
"completed": []
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async apiHandler(context, actionFn) {
|
|
103
|
+
// Do context stuff;
|
|
104
|
+
return await actionFn(context);
|
|
105
|
+
}
|
|
106
|
+
async pathHandler(context, actionFn) {
|
|
107
|
+
// Do context stuff;
|
|
108
|
+
return await actionFn(context);
|
|
109
|
+
}
|
|
110
|
+
async fallbackHandler(context) {
|
|
111
|
+
return die404("Not Found");
|
|
112
|
+
}
|
|
113
|
+
addApi(condition, actionFn) {
|
|
114
|
+
let conditionFn = (renderContext) => false;
|
|
115
|
+
if (typeof condition === "string") {
|
|
116
|
+
conditionFn = (renderContext) => renderContext.url === this.apiPath && (renderContext.post?.api) === condition;
|
|
117
|
+
}
|
|
118
|
+
else if (typeof condition === "function") {
|
|
119
|
+
conditionFn = (renderContext) => renderContext.url === this.apiPath && condition(renderContext);
|
|
120
|
+
}
|
|
121
|
+
else if (condition?.constructor == RegExp) {
|
|
122
|
+
conditionFn = (renderContext) => renderContext.url === this.apiPath && condition.test(renderContext.post?.api);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
throw "Unsupported API Condition";
|
|
126
|
+
}
|
|
127
|
+
return this._actionList.push({
|
|
128
|
+
conditionFn,
|
|
129
|
+
actionFn: async (renderContext) => await this.apiHandler(renderContext, actionFn)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
addPath(condition, actionFn) {
|
|
133
|
+
let conditionFn = (renderContext) => false;
|
|
134
|
+
if (typeof condition === "string") {
|
|
135
|
+
conditionFn = (renderContext) => renderContext.url === condition;
|
|
136
|
+
}
|
|
137
|
+
else if (typeof condition === "function") {
|
|
138
|
+
conditionFn = (renderContext) => condition(renderContext);
|
|
139
|
+
}
|
|
140
|
+
else if (condition?.constructor == RegExp) {
|
|
141
|
+
conditionFn = (renderContext) => condition.test(renderContext.url);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
throw "Unsupported Path Condition";
|
|
145
|
+
}
|
|
146
|
+
return this._actionList.push({
|
|
147
|
+
conditionFn,
|
|
148
|
+
actionFn: async (renderContext) => await this.pathHandler(renderContext, actionFn)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async addHook(when, hookFn, priority = 0) {
|
|
152
|
+
if (when === "created") {
|
|
153
|
+
await hookFn(this);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this._hookList[when].push({ priority, hookFn });
|
|
157
|
+
this._hookList[when].sort((a, b) => a.priority - b.priority);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async addModule(moduleFn) {
|
|
161
|
+
await moduleFn(this);
|
|
162
|
+
}
|
|
163
|
+
async render(event, runContext) {
|
|
164
|
+
return await new Promise(async (resolve, reject) => {
|
|
165
|
+
const eventContext = resolveEvent(event, resolve, reject);
|
|
166
|
+
const renderContext = { ...eventContext, runContext, resolve, reject };
|
|
167
|
+
if (renderContext.method === "OPTIONS")
|
|
168
|
+
return dieCors();
|
|
169
|
+
let firstMatchedAction = null;
|
|
170
|
+
for (const action of this._actionList) {
|
|
171
|
+
const isConditionMet = await action.conditionFn(renderContext);
|
|
172
|
+
if (isConditionMet) {
|
|
173
|
+
firstMatchedAction = action;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (firstMatchedAction) {
|
|
178
|
+
for (const hook of this._hookList["beforeRender"])
|
|
179
|
+
await hook.hookFn(renderContext);
|
|
180
|
+
let renderResult = await firstMatchedAction.actionFn(renderContext);
|
|
181
|
+
for (const hook of this._hookList["afterRender"]) {
|
|
182
|
+
renderResult = await hook.hookFn(renderContext, renderResult);
|
|
183
|
+
}
|
|
184
|
+
resolve(renderResult);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
for (const hook of this._hookList["fallback"])
|
|
188
|
+
await hook.hookFn(renderContext);
|
|
189
|
+
const renderResult = await this.fallbackHandler(renderContext);
|
|
190
|
+
resolve(renderResult);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
for (const hook of this._hookList["completed"])
|
|
194
|
+
await hook.hookFn(promiseResult);
|
|
195
|
+
}
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lambder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "exit 1",
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"lint": "eslint . --ext .ts,.tsx --fix"
|
|
12
|
+
},
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cookie": "^0.6.0",
|
|
17
|
+
"mime-types": "^2.1.35",
|
|
18
|
+
"querystring": "^0.2.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/aws-lambda": "^8.10.136",
|
|
22
|
+
"@types/cookie": "^0.6.0",
|
|
23
|
+
"@types/mime-types": "^2.1.4",
|
|
24
|
+
"@types/node": "^20.11.27",
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
26
|
+
"@typescript-eslint/parser": "^7.2.0",
|
|
27
|
+
"eslint": "^8.57.0",
|
|
28
|
+
"typescript": "^5.4.2"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/Resolver.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import * as pathLibrary from "path";
|
|
3
|
+
import mimeTypeResolver from "mime-types";
|
|
4
|
+
|
|
5
|
+
const convertToMultiHeader = (
|
|
6
|
+
headers: { [key:string]: string|string[] } | undefined
|
|
7
|
+
):{ [key:string]: string[] } =>
|
|
8
|
+
Object.fromEntries(Object.entries(headers||{}).map(([k,v])=>[ k, Array.isArray(v) ? v : [v] ]));
|
|
9
|
+
|
|
10
|
+
const CORS_HEADERS = convertToMultiHeader({
|
|
11
|
+
"Access-Control-Allow-Credentials": "true",
|
|
12
|
+
"Access-Control-Allow-Headers" : "Origin, X-Requested-With, Content-Type, Accept",
|
|
13
|
+
"Access-Control-Allow-Origin": "*",
|
|
14
|
+
"Access-Control-Allow-Methods": "OPTIONS,POST",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export type ResolverResponse = {
|
|
19
|
+
statusCode: number,
|
|
20
|
+
multiValueHeaders?: { [key:string]: string[] },
|
|
21
|
+
body: string | null,
|
|
22
|
+
isBase64Encoded?: boolean,
|
|
23
|
+
[key:string]: any,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
interface IResolverMethods {
|
|
28
|
+
raw(param: {
|
|
29
|
+
statusCode: number,
|
|
30
|
+
isBase64Encoded?: boolean,
|
|
31
|
+
body: null | string,
|
|
32
|
+
multiValueHeaders: { [key: string]: string[] },
|
|
33
|
+
}): any;
|
|
34
|
+
json(data: { [key: string]: any }, headers?: { [key: string]: string | string[] }): any;
|
|
35
|
+
xml(data: string): any;
|
|
36
|
+
html(data: string, headers?: { [key: string]: string | string[] }): any;
|
|
37
|
+
status301(url: string, headers?: { [key: string]: string | string[] }): any;
|
|
38
|
+
status404(data: string, headers?: { [key: string]: string | string[] }): any;
|
|
39
|
+
cors(): any;
|
|
40
|
+
fileBase64(fileBase64: string, mimeType: string, headers?: { [key: string]: string | string[] }): any;
|
|
41
|
+
file(filePath: string, headers?: { [key: string]: string | string[] }): any;
|
|
42
|
+
api({ payload, error, ...other }: { payload?: any; error?: any; [key: string]: any; }, headers?: { [key: string]: string | string[] }): any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export default class Resolver {
|
|
47
|
+
private isCorsEnabled: boolean;
|
|
48
|
+
private publicPath: string;
|
|
49
|
+
private apiVersion: string|null;
|
|
50
|
+
resolve: Function;
|
|
51
|
+
reject: Function;
|
|
52
|
+
die: Partial<IResolverMethods>;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
{ isCorsEnabled, publicPath, apiVersion, resolve, reject }:
|
|
56
|
+
{
|
|
57
|
+
isCorsEnabled: boolean,
|
|
58
|
+
publicPath: string,
|
|
59
|
+
apiVersion?: string|null,
|
|
60
|
+
resolve:(renderResult: ResolverResponse)=>void,
|
|
61
|
+
reject:(err: Error)=>void
|
|
62
|
+
}
|
|
63
|
+
){
|
|
64
|
+
this.isCorsEnabled = isCorsEnabled;
|
|
65
|
+
this.publicPath = publicPath;
|
|
66
|
+
this.apiVersion = apiVersion ?? null;
|
|
67
|
+
this.resolve = resolve;
|
|
68
|
+
this.reject = reject;
|
|
69
|
+
this.die = new Proxy(this, {
|
|
70
|
+
get: (target, prop: keyof IResolverMethods) => {
|
|
71
|
+
const origProp = target[prop];
|
|
72
|
+
if (typeof origProp === 'function') {
|
|
73
|
+
return (...args: any[]) => {
|
|
74
|
+
this.resolve((origProp as Function).apply(this, args));
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return origProp;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private readFileSync(filePath: string){
|
|
83
|
+
const __dirname = pathLibrary.resolve(pathLibrary.dirname(""));
|
|
84
|
+
const absolutePath = pathLibrary.resolve(filePath);
|
|
85
|
+
const publicPath = pathLibrary.resolve(this.publicPath);
|
|
86
|
+
if(!absolutePath.includes(publicPath)){ return "forbiddenpath"; }
|
|
87
|
+
return fs.readFileSync(filePath);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
raw(param: ResolverResponse){
|
|
91
|
+
return param;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
json(
|
|
95
|
+
data: { [key:string]: any },
|
|
96
|
+
headers?: { [key:string]: string|string[] }
|
|
97
|
+
):ResolverResponse{
|
|
98
|
+
return this.raw({
|
|
99
|
+
statusCode: 200,
|
|
100
|
+
multiValueHeaders: {
|
|
101
|
+
"Content-Type": ["application/json; charset=utf-8"],
|
|
102
|
+
...(this.isCorsEnabled ? CORS_HEADERS : {}),
|
|
103
|
+
...convertToMultiHeader(headers)
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(data),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
xml(data: string):ResolverResponse{
|
|
110
|
+
return this.raw({
|
|
111
|
+
statusCode: 200,
|
|
112
|
+
isBase64Encoded: true,
|
|
113
|
+
multiValueHeaders: { "Content-Type": ["application/xml; charset=utf-8"]},
|
|
114
|
+
body: Buffer.from(data).toString("base64"),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
html(
|
|
119
|
+
data: string,
|
|
120
|
+
headers?: { [key:string]: string|string[] },
|
|
121
|
+
):ResolverResponse{
|
|
122
|
+
return this.raw({
|
|
123
|
+
statusCode: 200,
|
|
124
|
+
isBase64Encoded: true,
|
|
125
|
+
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
126
|
+
body: Buffer.from(data).toString("base64"),
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
status301(
|
|
131
|
+
url: string,
|
|
132
|
+
headers?: { [key:string]: string|string[] },
|
|
133
|
+
):ResolverResponse{
|
|
134
|
+
return this.raw({
|
|
135
|
+
statusCode: 301,
|
|
136
|
+
multiValueHeaders: { "Location" : [url], ...convertToMultiHeader(headers) },
|
|
137
|
+
body:null
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
status404(
|
|
142
|
+
data: string,
|
|
143
|
+
headers?: { [key:string]: string|string[] },
|
|
144
|
+
):ResolverResponse{
|
|
145
|
+
return this.raw({
|
|
146
|
+
statusCode: 404,
|
|
147
|
+
isBase64Encoded: true,
|
|
148
|
+
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
149
|
+
body: Buffer.from(data).toString("base64"),
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
cors():ResolverResponse{
|
|
154
|
+
return this.raw({
|
|
155
|
+
statusCode: 200,
|
|
156
|
+
multiValueHeaders: this.isCorsEnabled ? CORS_HEADERS: {},
|
|
157
|
+
body: JSON.stringify(""),
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
fileBase64 (
|
|
162
|
+
fileBase64: string,
|
|
163
|
+
mimeType: string,
|
|
164
|
+
headers?: { [key:string]: string|string[] },
|
|
165
|
+
):ResolverResponse{
|
|
166
|
+
return this.raw({
|
|
167
|
+
statusCode: 200,
|
|
168
|
+
isBase64Encoded: true,
|
|
169
|
+
multiValueHeaders: { "Content-Type": [mimeType || "text/html"], ...convertToMultiHeader(headers) },
|
|
170
|
+
body: fileBase64,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
file(
|
|
175
|
+
filePath: string,
|
|
176
|
+
headers?: { [key:string]: string|string[] },
|
|
177
|
+
):ResolverResponse{
|
|
178
|
+
if(!fs.existsSync(filePath)) return this.json({ error: "File not found: " + filePath });
|
|
179
|
+
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
180
|
+
const body = this.readFileSync(filePath);
|
|
181
|
+
const bodyBase64 = Buffer.from(body).toString("base64");
|
|
182
|
+
console.log("bodyBase64.length",bodyBase64.length);
|
|
183
|
+
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
api(
|
|
187
|
+
{ payload, error=null, ...other }: { payload?: any; error?: any; },
|
|
188
|
+
headers?: { [key:string]: string|string[] },
|
|
189
|
+
):ResolverResponse{
|
|
190
|
+
if(error) console.log("ERROR: ", error);
|
|
191
|
+
if(Object.keys(other).length) console.log("ERROR: Unexpected key in result: " + Object.keys(other).join(", "));
|
|
192
|
+
return this.json({
|
|
193
|
+
apiVersion: this.apiVersion,
|
|
194
|
+
payload,
|
|
195
|
+
...(error ? {error} : {}),
|
|
196
|
+
}, headers);
|
|
197
|
+
};
|
|
198
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import querystring from "querystring";
|
|
2
|
+
import cookieParser from "cookie";
|
|
3
|
+
|
|
4
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
5
|
+
import Resolver, { ResolverResponse } from "./Resolver";
|
|
6
|
+
|
|
7
|
+
type RawResolvedContext = {
|
|
8
|
+
host: string;
|
|
9
|
+
url: string;
|
|
10
|
+
method: string;
|
|
11
|
+
get: { [key: string]: any };
|
|
12
|
+
post: { [key: string]: any };
|
|
13
|
+
cookie: { [key: string]: any };
|
|
14
|
+
headers: APIGatewayProxyEventHeaders;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type RenderContext = RawResolvedContext & {
|
|
18
|
+
runContext: Context,
|
|
19
|
+
resolver: Resolver,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ConditionFunction = (renderContext: RenderContext) => boolean|Promise<boolean>;
|
|
23
|
+
type ActionFunction = (renderContext: RenderContext) => ResolverResponse|Promise<ResolverResponse>;
|
|
24
|
+
type ActionType = { conditionFn: ConditionFunction, actionFn: ActionFunction };
|
|
25
|
+
|
|
26
|
+
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
27
|
+
type HookBeforeRenderFunction = (renderContext: RenderContext) => Promise<void>;
|
|
28
|
+
type HookAfterRenderFunction = (renderContext: RenderContext, renderResult: ResolverResponse) => ResolverResponse|Promise<ResolverResponse>;
|
|
29
|
+
type HookFallbackFunction = (renderContext: RenderContext) => ResolverResponse|Promise<ResolverResponse>;
|
|
30
|
+
type HookCompletedFunction = (renderResult: ResolverResponse) => ResolverResponse|Promise<ResolverResponse>;
|
|
31
|
+
|
|
32
|
+
type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback"|"completed";
|
|
33
|
+
type HookListType = {
|
|
34
|
+
priority: number,
|
|
35
|
+
hookFn: HookCreatedFunction|HookBeforeRenderFunction|HookAfterRenderFunction|HookFallbackFunction|HookCompletedFunction,
|
|
36
|
+
}[];
|
|
37
|
+
|
|
38
|
+
export const resolveEvent = (
|
|
39
|
+
event: APIGatewayProxyEvent,
|
|
40
|
+
resolve: Function,
|
|
41
|
+
reject: Function
|
|
42
|
+
):RawResolvedContext => {
|
|
43
|
+
const host = event.headers.Host || event.headers.host || "";
|
|
44
|
+
const url = event.path;
|
|
45
|
+
const get: { [key:string]: any } = event.queryStringParameters || {};
|
|
46
|
+
const method = event.httpMethod;
|
|
47
|
+
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
48
|
+
const headers = event.headers;
|
|
49
|
+
let post:{ [key:string]: any } = {};
|
|
50
|
+
try {
|
|
51
|
+
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
52
|
+
try { post = JSON.parse(decodedBody) || {}; }
|
|
53
|
+
catch(e){ post = querystring.parse(decodedBody) || {}; }
|
|
54
|
+
}catch(e){}
|
|
55
|
+
if(/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)){ reject(new Error("[404] Host cannot be an ip address: " + host)); }
|
|
56
|
+
return { host, url, method, get, post, cookie, headers };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
export default class Lambder {
|
|
61
|
+
private apiPath: string;
|
|
62
|
+
private apiVersion: null|string;
|
|
63
|
+
private isCorsEnabled: boolean;
|
|
64
|
+
private publicPath: string;
|
|
65
|
+
|
|
66
|
+
private _actionList: ActionType[];
|
|
67
|
+
private _hookList: {
|
|
68
|
+
"beforeRender": { priority: number, hookFn: HookBeforeRenderFunction }[],
|
|
69
|
+
"afterRender": { priority: number, hookFn: HookAfterRenderFunction }[],
|
|
70
|
+
"fallback": { priority: number, hookFn: HookFallbackFunction }[],
|
|
71
|
+
"completed": { priority: number, hookFn: HookCompletedFunction }[]
|
|
72
|
+
};
|
|
73
|
+
private globalErrorHandler: Function|null = null;
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
{ apiPath, apiVersion, isCorsEnabled, publicPath }:
|
|
77
|
+
{ apiPath?: string, apiVersion?: string, isCorsEnabled?: boolean, publicPath: string }
|
|
78
|
+
){
|
|
79
|
+
this.apiPath = apiPath ?? "/api";
|
|
80
|
+
this.apiVersion = apiVersion ?? null;
|
|
81
|
+
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
82
|
+
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
83
|
+
|
|
84
|
+
this._actionList = [];
|
|
85
|
+
this._hookList = {
|
|
86
|
+
"beforeRender": [],
|
|
87
|
+
"afterRender": [],
|
|
88
|
+
"fallback": [],
|
|
89
|
+
"completed": []
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async apiHandler (renderContext:RenderContext, actionFn: ActionFunction){
|
|
94
|
+
// Do context stuff;
|
|
95
|
+
return await actionFn(renderContext);
|
|
96
|
+
}
|
|
97
|
+
private async pathHandler (renderContext:RenderContext, actionFn: ActionFunction){
|
|
98
|
+
// Do context stuff;
|
|
99
|
+
return await actionFn(renderContext);
|
|
100
|
+
}
|
|
101
|
+
private async fallbackHandler(renderContext:RenderContext){
|
|
102
|
+
return renderContext.resolver.file("index.html");
|
|
103
|
+
}
|
|
104
|
+
setGlobalErrorHandler(globalErrorHandler: (err: Error)=>ResolverResponse){
|
|
105
|
+
this.globalErrorHandler = globalErrorHandler;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
addApi(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
109
|
+
let conditionFn: ConditionFunction = (renderContext:RenderContext) => false;
|
|
110
|
+
if(typeof condition === "string"){
|
|
111
|
+
conditionFn = (renderContext:RenderContext) => renderContext.url === this.apiPath && (renderContext.post?.api) === condition;
|
|
112
|
+
}else if(typeof condition === "function"){
|
|
113
|
+
conditionFn = async (renderContext:RenderContext) => renderContext.url === this.apiPath && (await condition(renderContext));
|
|
114
|
+
}else if(condition?.constructor == RegExp){
|
|
115
|
+
conditionFn = (renderContext:RenderContext) => renderContext.url === this.apiPath && condition.test(renderContext.post?.api);
|
|
116
|
+
}else { throw "Unsupported API Condition"; }
|
|
117
|
+
this._actionList.push({
|
|
118
|
+
conditionFn,
|
|
119
|
+
actionFn: async (renderContext:RenderContext) => await this.apiHandler(renderContext, actionFn)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addPath(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
124
|
+
let conditionFn: ConditionFunction = (renderContext:RenderContext) => false;
|
|
125
|
+
if(typeof condition === "string"){
|
|
126
|
+
conditionFn = (renderContext:RenderContext) => renderContext.url === condition;
|
|
127
|
+
}else if(typeof condition === "function"){
|
|
128
|
+
conditionFn = async (renderContext:RenderContext) => (await condition(renderContext));
|
|
129
|
+
}else if(condition?.constructor == RegExp){
|
|
130
|
+
conditionFn = (renderContext:RenderContext) => condition.test(renderContext.url);
|
|
131
|
+
}else { throw "Unsupported Path Condition"; }
|
|
132
|
+
this._actionList.push({
|
|
133
|
+
conditionFn,
|
|
134
|
+
actionFn: async (renderContext:RenderContext) => await this.pathHandler(renderContext, actionFn)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
141
|
+
async addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
142
|
+
async addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
143
|
+
async addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
144
|
+
async addHook(hookEvent: 'completed', hookFn: HookCompletedFunction, priority?: number): Promise<void>;
|
|
145
|
+
async addHook(
|
|
146
|
+
hookEvent:HookEventType,
|
|
147
|
+
hookFn: HookCreatedFunction & HookBeforeRenderFunction & HookAfterRenderFunction & HookFallbackFunction & HookCompletedFunction,
|
|
148
|
+
priority = 0
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
if(hookEvent === "created"){
|
|
151
|
+
await hookFn(this);
|
|
152
|
+
}else{
|
|
153
|
+
this._hookList[hookEvent].push({ priority, hookFn });
|
|
154
|
+
this._hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async addModule(moduleFn: Function): Promise<void>{
|
|
159
|
+
await moduleFn(this);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async render(
|
|
163
|
+
event: APIGatewayProxyEvent,
|
|
164
|
+
runContext: Context
|
|
165
|
+
): Promise<ResolverResponse>{
|
|
166
|
+
return await new Promise(async (
|
|
167
|
+
resolve:(renderResult: ResolverResponse)=>void,
|
|
168
|
+
reject:(err: Error)=>void
|
|
169
|
+
)=> {
|
|
170
|
+
|
|
171
|
+
const resolver = new Resolver({
|
|
172
|
+
isCorsEnabled: this.isCorsEnabled,
|
|
173
|
+
publicPath: this.publicPath,
|
|
174
|
+
apiVersion: this.apiVersion,
|
|
175
|
+
resolve, reject,
|
|
176
|
+
});
|
|
177
|
+
const eventContext:RawResolvedContext = resolveEvent(event, resolve, reject);
|
|
178
|
+
const renderContext:RenderContext = { ...eventContext, runContext, resolver };
|
|
179
|
+
|
|
180
|
+
if(renderContext.method === "OPTIONS") return resolver.cors();
|
|
181
|
+
|
|
182
|
+
let firstMatchedAction: null|ActionType = null;
|
|
183
|
+
for(const action of this._actionList){
|
|
184
|
+
const isConditionMet = await action.conditionFn(renderContext);
|
|
185
|
+
if(isConditionMet){
|
|
186
|
+
firstMatchedAction = action;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if(firstMatchedAction){
|
|
192
|
+
for(const hook of this._hookList["beforeRender"]) await hook.hookFn(renderContext);
|
|
193
|
+
let renderResult = await firstMatchedAction.actionFn(renderContext);
|
|
194
|
+
for(const hook of this._hookList["afterRender"]){
|
|
195
|
+
renderResult = await hook.hookFn(renderContext, renderResult);
|
|
196
|
+
}
|
|
197
|
+
resolve(renderResult);
|
|
198
|
+
}else{
|
|
199
|
+
for(const hook of this._hookList["fallback"]) await hook.hookFn(renderContext);
|
|
200
|
+
const renderResult = await this.fallbackHandler(renderContext);
|
|
201
|
+
resolve(renderResult);
|
|
202
|
+
}
|
|
203
|
+
}).then(async (renderResult: ResolverResponse):Promise<ResolverResponse> => {
|
|
204
|
+
for(const hook of this._hookList["completed"]){
|
|
205
|
+
renderResult = await hook.hookFn(renderResult);
|
|
206
|
+
}
|
|
207
|
+
return renderResult;
|
|
208
|
+
}).catch(async (err: Error):Promise<ResolverResponse> => {
|
|
209
|
+
if(this.globalErrorHandler){
|
|
210
|
+
return this.globalErrorHandler(err);
|
|
211
|
+
}
|
|
212
|
+
return { statusCode: 501, body: "Unknown Server Error", }
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext", // Compile to a modern ECMAScript version that supports modules
|
|
4
|
+
"module": "ESNext", // Use ESNext as module code generation to support ESM
|
|
5
|
+
"declaration": true, // Generate corresponding '.d.ts' file.
|
|
6
|
+
"outDir": "./dist", // Redirect output structure to the 'dist' directory
|
|
7
|
+
"strict": true, // Enable all strict type-checking options
|
|
8
|
+
"esModuleInterop": true, // Enables compatibility with Babel imports
|
|
9
|
+
"skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts)
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "node", // Module resolution strategy to use
|
|
12
|
+
"resolveJsonModule": true, // Allow importing of '.json' files
|
|
13
|
+
"isolatedModules": true, // Ensure each file can be safely transpiled without relying on other imports
|
|
14
|
+
"baseUrl": ".", // Base directory to resolve non-relative module names
|
|
15
|
+
"paths": { // Specify paths for imports
|
|
16
|
+
"*": ["node_modules/*"]
|
|
17
|
+
},
|
|
18
|
+
"lib": ["ESNext", "DOM"], // Specify library files to be included in the compilation
|
|
19
|
+
"types": ["node"] // Type definitions to be included
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*"], // Include all files in the src directory
|
|
22
|
+
"exclude": ["node_modules", "dist"] // Exclude 'node_modules' directory and 'dist' directory
|
|
23
|
+
}
|
|
24
|
+
|