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 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
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "editor.codeActionsOnSave": {
3
+ "source.fixAll.eslint": true
4
+ },
5
+ "eslint.validate": [
6
+ "javascript",
7
+ "javascriptreact",
8
+ "typescript",
9
+ "typescriptreact"
10
+ ],
11
+ "eslint.workingDirectories": [
12
+ "./src"
13
+ ]
14
+ }
15
+
@@ -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
+ }
@@ -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
+