lambder 1.0.24 → 1.0.26
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/Lambder.d.ts +72 -0
- package/dist/Lambder.js +191 -0
- package/dist/Resolver.d.ts +6 -3
- package/dist/index.d.ts +2 -71
- package/dist/index.js +2 -191
- package/package.json +1 -1
- package/src/Lambder.ts +271 -0
- package/src/Resolver.ts +14 -1
- package/src/index.ts +2 -271
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
2
|
+
import Resolver, { ResolverResponse } from "./Resolver.js";
|
|
3
|
+
type DDBSession = {
|
|
4
|
+
userId: string;
|
|
5
|
+
userIdHash: string;
|
|
6
|
+
sessionHash: string;
|
|
7
|
+
csrfToken: string;
|
|
8
|
+
createdTimeStamp: number;
|
|
9
|
+
expiresTimeStamp: number;
|
|
10
|
+
data: {
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
} | null;
|
|
14
|
+
type RenderContext = {
|
|
15
|
+
host: string;
|
|
16
|
+
url: string;
|
|
17
|
+
method: string;
|
|
18
|
+
get: {
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
};
|
|
21
|
+
post: {
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
cookie: {
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
};
|
|
27
|
+
headers: APIGatewayProxyEventHeaders;
|
|
28
|
+
session: DDBSession;
|
|
29
|
+
lambdaContext: Context;
|
|
30
|
+
};
|
|
31
|
+
type ConditionFunction = (renderContext: RenderContext) => boolean | Promise<boolean>;
|
|
32
|
+
type ActionFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse | Promise<ResolverResponse>;
|
|
33
|
+
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
34
|
+
type HookBeforeRenderFunction = (renderContext: RenderContext, resolver: Resolver) => RenderContext | Promise<RenderContext>;
|
|
35
|
+
type HookAfterRenderFunction = (renderContext: RenderContext, resolver: Resolver, renderResult: ResolverResponse) => ResolverResponse | Promise<ResolverResponse>;
|
|
36
|
+
type HookFallbackFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse | Promise<ResolverResponse>;
|
|
37
|
+
type HookCompletedFunction = (renderResult: ResolverResponse) => ResolverResponse | Promise<ResolverResponse>;
|
|
38
|
+
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, resolve: Function, reject: Function) => RenderContext;
|
|
39
|
+
export default class Lambder {
|
|
40
|
+
private apiPath;
|
|
41
|
+
private apiVersion;
|
|
42
|
+
private isCorsEnabled;
|
|
43
|
+
private publicPath;
|
|
44
|
+
private _actionList;
|
|
45
|
+
private _hookList;
|
|
46
|
+
private globalErrorHandler;
|
|
47
|
+
private routeFallbackHandler;
|
|
48
|
+
private apiFallbackHandler;
|
|
49
|
+
constructor({ publicPath, apiPath, apiVersion, isCorsEnabled, }: {
|
|
50
|
+
publicPath: string;
|
|
51
|
+
apiPath?: string;
|
|
52
|
+
apiVersion?: string;
|
|
53
|
+
isCorsEnabled?: boolean;
|
|
54
|
+
});
|
|
55
|
+
setRouteFallbackHandler(routeFallbackHandler: (renderContext: RenderContext, resolver: Resolver) => ResolverResponse): void;
|
|
56
|
+
setApiFallbackHandler(apiFallbackHandler: (renderContext: RenderContext, resolver: Resolver) => ResolverResponse): void;
|
|
57
|
+
setGlobalErrorHandler(globalErrorHandler: (err: Error) => ResolverResponse): void;
|
|
58
|
+
private validateSession;
|
|
59
|
+
private handleNoMatchedAction;
|
|
60
|
+
addModule(moduleFn: Function): Promise<void>;
|
|
61
|
+
addRoute(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
62
|
+
addSessionRoute(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
63
|
+
addApi(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
64
|
+
addSessionApi(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
65
|
+
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
66
|
+
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
67
|
+
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
68
|
+
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
69
|
+
addHook(hookEvent: 'completed', hookFn: HookCompletedFunction, priority?: number): Promise<void>;
|
|
70
|
+
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<ResolverResponse>;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
package/dist/Lambder.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import querystring from "querystring";
|
|
2
|
+
import cookieParser from "cookie";
|
|
3
|
+
import Resolver from "./Resolver.js";
|
|
4
|
+
export const createContext = (event, lambdaContext, resolve, reject) => {
|
|
5
|
+
const host = event.headers.Host || event.headers.host || "";
|
|
6
|
+
const url = event.path;
|
|
7
|
+
const get = event.queryStringParameters || {};
|
|
8
|
+
const method = event.httpMethod;
|
|
9
|
+
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
10
|
+
const headers = event.headers;
|
|
11
|
+
let post = {};
|
|
12
|
+
const session = null;
|
|
13
|
+
try {
|
|
14
|
+
const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
|
|
15
|
+
try {
|
|
16
|
+
post = JSON.parse(decodedBody) || {};
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
post = querystring.parse(decodedBody) || {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (e) { }
|
|
23
|
+
if (/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)) {
|
|
24
|
+
reject(new Error("[404] Host cannot be an ip address: " + host));
|
|
25
|
+
}
|
|
26
|
+
return { host, url, method, get, post, cookie, headers, session, lambdaContext };
|
|
27
|
+
};
|
|
28
|
+
export default class Lambder {
|
|
29
|
+
apiPath;
|
|
30
|
+
apiVersion;
|
|
31
|
+
isCorsEnabled;
|
|
32
|
+
publicPath;
|
|
33
|
+
_actionList;
|
|
34
|
+
_hookList;
|
|
35
|
+
globalErrorHandler = null;
|
|
36
|
+
routeFallbackHandler = null;
|
|
37
|
+
apiFallbackHandler = null;
|
|
38
|
+
constructor({ publicPath, apiPath, apiVersion, isCorsEnabled, }) {
|
|
39
|
+
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
40
|
+
this.apiPath = apiPath ?? "/api";
|
|
41
|
+
this.apiVersion = apiVersion ?? null;
|
|
42
|
+
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
43
|
+
this._actionList = [];
|
|
44
|
+
this._hookList = {
|
|
45
|
+
"beforeRender": [],
|
|
46
|
+
"afterRender": [],
|
|
47
|
+
"fallback": [],
|
|
48
|
+
"completed": []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
setRouteFallbackHandler(routeFallbackHandler) {
|
|
52
|
+
this.routeFallbackHandler = routeFallbackHandler;
|
|
53
|
+
}
|
|
54
|
+
setApiFallbackHandler(apiFallbackHandler) {
|
|
55
|
+
this.apiFallbackHandler = apiFallbackHandler;
|
|
56
|
+
}
|
|
57
|
+
setGlobalErrorHandler(globalErrorHandler) {
|
|
58
|
+
this.globalErrorHandler = globalErrorHandler;
|
|
59
|
+
}
|
|
60
|
+
async validateSession(session) {
|
|
61
|
+
if (!session)
|
|
62
|
+
return false;
|
|
63
|
+
if (!session.userId || !session.userIdHash || !session.sessionHash || !session.csrfToken)
|
|
64
|
+
return false;
|
|
65
|
+
if (!session.createdTimeStamp || !session.expiresTimeStamp)
|
|
66
|
+
return false;
|
|
67
|
+
if (session.expiresTimeStamp > Date.now())
|
|
68
|
+
return false;
|
|
69
|
+
// Check DDB;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
;
|
|
73
|
+
async handleNoMatchedAction(renderContext, resolver) {
|
|
74
|
+
for (const hook of this._hookList["fallback"]) {
|
|
75
|
+
await hook.hookFn(renderContext, resolver);
|
|
76
|
+
}
|
|
77
|
+
if (renderContext.url === this.apiPath) {
|
|
78
|
+
if (this.apiFallbackHandler) {
|
|
79
|
+
resolver.resolve(await this.apiFallbackHandler(renderContext, resolver));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
resolver.resolve({ statusCode: 204, body: "API handler not set.", });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (this.routeFallbackHandler) {
|
|
86
|
+
resolver.resolve(await this.routeFallbackHandler(renderContext, resolver));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
resolver.resolve({ statusCode: 204, body: "Handler not set.", });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async addModule(moduleFn) {
|
|
93
|
+
await moduleFn(this);
|
|
94
|
+
}
|
|
95
|
+
addRoute(condition, actionFn) {
|
|
96
|
+
this._actionList.push({
|
|
97
|
+
conditionFn: async (renderContext) => (renderContext.method === "GET" &&
|
|
98
|
+
((typeof condition === "string" && renderContext.url === condition) ||
|
|
99
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
100
|
+
(condition?.constructor == RegExp && condition.test(renderContext.url)))),
|
|
101
|
+
actionFn: async (renderContext, resolver) => await actionFn(renderContext, resolver)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
;
|
|
105
|
+
addSessionRoute(condition, actionFn) {
|
|
106
|
+
this._actionList.push({
|
|
107
|
+
conditionFn: async (renderContext) => (renderContext.method === "GET" &&
|
|
108
|
+
((typeof condition === "string" && renderContext.url === condition) ||
|
|
109
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
110
|
+
(condition?.constructor == RegExp && condition.test(renderContext.url)))),
|
|
111
|
+
actionFn: async (renderContext, resolver) => await actionFn(renderContext, resolver)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
;
|
|
115
|
+
addApi(condition, actionFn) {
|
|
116
|
+
this._actionList.push({
|
|
117
|
+
conditionFn: async (renderContext) => (renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
118
|
+
((typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
119
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
120
|
+
(condition?.constructor == RegExp && condition.test(renderContext.post?.api)))),
|
|
121
|
+
actionFn: async (renderContext, resolver) => await actionFn(renderContext, resolver)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
;
|
|
125
|
+
addSessionApi(condition, actionFn) {
|
|
126
|
+
this._actionList.push({
|
|
127
|
+
conditionFn: async (renderContext) => (renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
128
|
+
((typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
129
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
130
|
+
(condition?.constructor == RegExp && condition.test(renderContext.post?.api)))),
|
|
131
|
+
actionFn: async (renderContext, resolver) => {
|
|
132
|
+
const isSessionValid = this.validateSession(renderContext.session);
|
|
133
|
+
if (!isSessionValid)
|
|
134
|
+
throw new Error("Session not found");
|
|
135
|
+
return await actionFn(renderContext, resolver);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
;
|
|
140
|
+
async addHook(hookEvent, hookFn, priority = 0) {
|
|
141
|
+
if (hookEvent === "created") {
|
|
142
|
+
await hookFn(this);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this._hookList[hookEvent].push({ priority, hookFn });
|
|
146
|
+
this._hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async render(event, lambdaContext) {
|
|
150
|
+
return await new Promise(async (resolve, reject) => {
|
|
151
|
+
const resolver = new Resolver({
|
|
152
|
+
isCorsEnabled: this.isCorsEnabled,
|
|
153
|
+
publicPath: this.publicPath,
|
|
154
|
+
apiVersion: this.apiVersion,
|
|
155
|
+
resolve, reject,
|
|
156
|
+
});
|
|
157
|
+
let renderContext = createContext(event, lambdaContext, resolve, reject);
|
|
158
|
+
if (renderContext.method === "OPTIONS")
|
|
159
|
+
return resolver.cors();
|
|
160
|
+
const firstMatchedAction = await (async () => {
|
|
161
|
+
for (const action of this._actionList) {
|
|
162
|
+
if (await action.conditionFn(renderContext))
|
|
163
|
+
return action;
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
if (firstMatchedAction) {
|
|
167
|
+
for (const hook of this._hookList["beforeRender"]) {
|
|
168
|
+
renderContext = await hook.hookFn(renderContext, resolver);
|
|
169
|
+
}
|
|
170
|
+
let renderResult = await firstMatchedAction.actionFn(renderContext, resolver);
|
|
171
|
+
for (const hook of this._hookList["afterRender"]) {
|
|
172
|
+
renderResult = await hook.hookFn(renderContext, resolver, renderResult);
|
|
173
|
+
}
|
|
174
|
+
resolve(renderResult);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
return this.handleNoMatchedAction(renderContext, resolver);
|
|
178
|
+
}
|
|
179
|
+
}).then(async (renderResult) => {
|
|
180
|
+
for (const hook of this._hookList["completed"]) {
|
|
181
|
+
renderResult = await hook.hookFn(renderResult);
|
|
182
|
+
}
|
|
183
|
+
return renderResult;
|
|
184
|
+
}).catch(async (err) => {
|
|
185
|
+
if (this.globalErrorHandler) {
|
|
186
|
+
return this.globalErrorHandler(err);
|
|
187
|
+
}
|
|
188
|
+
return { statusCode: 500, body: "Internal Server Error.", };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
package/dist/Resolver.d.ts
CHANGED
|
@@ -38,10 +38,13 @@ interface IResolverMethods {
|
|
|
38
38
|
file(filePath: string, headers?: {
|
|
39
39
|
[key: string]: string | string[];
|
|
40
40
|
}): any;
|
|
41
|
-
api({ payload,
|
|
41
|
+
api({ payload, versionExpired, sessionExpired, notAuthorized, message, errorMessage, ...other }: {
|
|
42
42
|
payload?: any;
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
versionExpired?: boolean;
|
|
44
|
+
sessionExpired?: boolean;
|
|
45
|
+
notAuthorized?: boolean;
|
|
46
|
+
message?: any;
|
|
47
|
+
errorMessage?: any;
|
|
45
48
|
}, headers?: {
|
|
46
49
|
[key: string]: string | string[];
|
|
47
50
|
}): any;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,72 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import Lambder from './Lambder.js';
|
|
2
|
+
export default Lambder;
|
|
3
3
|
export { default as LambderCaller } from "./LambderCaller.js";
|
|
4
|
-
type DDBSession = {
|
|
5
|
-
userId: string;
|
|
6
|
-
userIdHash: string;
|
|
7
|
-
sessionHash: string;
|
|
8
|
-
csrfToken: string;
|
|
9
|
-
createdTimeStamp: number;
|
|
10
|
-
expiresTimeStamp: number;
|
|
11
|
-
data: {
|
|
12
|
-
[key: string]: any;
|
|
13
|
-
};
|
|
14
|
-
} | null;
|
|
15
|
-
type RenderContext = {
|
|
16
|
-
host: string;
|
|
17
|
-
url: string;
|
|
18
|
-
method: string;
|
|
19
|
-
get: {
|
|
20
|
-
[key: string]: any;
|
|
21
|
-
};
|
|
22
|
-
post: {
|
|
23
|
-
[key: string]: any;
|
|
24
|
-
};
|
|
25
|
-
cookie: {
|
|
26
|
-
[key: string]: any;
|
|
27
|
-
};
|
|
28
|
-
headers: APIGatewayProxyEventHeaders;
|
|
29
|
-
session: DDBSession;
|
|
30
|
-
lambdaContext: Context;
|
|
31
|
-
};
|
|
32
|
-
type ConditionFunction = (renderContext: RenderContext) => boolean | Promise<boolean>;
|
|
33
|
-
type ActionFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse | Promise<ResolverResponse>;
|
|
34
|
-
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
35
|
-
type HookBeforeRenderFunction = (renderContext: RenderContext, resolver: Resolver) => RenderContext | Promise<RenderContext>;
|
|
36
|
-
type HookAfterRenderFunction = (renderContext: RenderContext, resolver: Resolver, renderResult: ResolverResponse) => ResolverResponse | Promise<ResolverResponse>;
|
|
37
|
-
type HookFallbackFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse | Promise<ResolverResponse>;
|
|
38
|
-
type HookCompletedFunction = (renderResult: ResolverResponse) => ResolverResponse | Promise<ResolverResponse>;
|
|
39
|
-
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, resolve: Function, reject: Function) => RenderContext;
|
|
40
|
-
export default class Lambder {
|
|
41
|
-
private apiPath;
|
|
42
|
-
private apiVersion;
|
|
43
|
-
private isCorsEnabled;
|
|
44
|
-
private publicPath;
|
|
45
|
-
private _actionList;
|
|
46
|
-
private _hookList;
|
|
47
|
-
private globalErrorHandler;
|
|
48
|
-
private routeFallbackHandler;
|
|
49
|
-
private apiFallbackHandler;
|
|
50
|
-
constructor({ publicPath, apiPath, apiVersion, isCorsEnabled, }: {
|
|
51
|
-
publicPath: string;
|
|
52
|
-
apiPath?: string;
|
|
53
|
-
apiVersion?: string;
|
|
54
|
-
isCorsEnabled?: boolean;
|
|
55
|
-
});
|
|
56
|
-
setRouteFallbackHandler(routeFallbackHandler: (renderContext: RenderContext, resolver: Resolver) => ResolverResponse): void;
|
|
57
|
-
setApiFallbackHandler(apiFallbackHandler: (renderContext: RenderContext, resolver: Resolver) => ResolverResponse): void;
|
|
58
|
-
setGlobalErrorHandler(globalErrorHandler: (err: Error) => ResolverResponse): void;
|
|
59
|
-
private validateSession;
|
|
60
|
-
private handleNoMatchedAction;
|
|
61
|
-
addModule(moduleFn: Function): Promise<void>;
|
|
62
|
-
addRoute(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
63
|
-
addSessionRoute(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
64
|
-
addApi(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
65
|
-
addSessionApi(condition: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
66
|
-
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
67
|
-
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
68
|
-
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
69
|
-
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
70
|
-
addHook(hookEvent: 'completed', hookFn: HookCompletedFunction, priority?: number): Promise<void>;
|
|
71
|
-
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<ResolverResponse>;
|
|
72
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -1,192 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import Resolver from "./Resolver.js";
|
|
1
|
+
import Lambder from './Lambder.js';
|
|
2
|
+
export default Lambder;
|
|
4
3
|
export { default as LambderCaller } from "./LambderCaller.js";
|
|
5
|
-
export const createContext = (event, lambdaContext, resolve, reject) => {
|
|
6
|
-
const host = event.headers.Host || event.headers.host || "";
|
|
7
|
-
const url = event.path;
|
|
8
|
-
const get = event.queryStringParameters || {};
|
|
9
|
-
const method = event.httpMethod;
|
|
10
|
-
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
11
|
-
const headers = event.headers;
|
|
12
|
-
let post = {};
|
|
13
|
-
const session = null;
|
|
14
|
-
try {
|
|
15
|
-
const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
|
|
16
|
-
try {
|
|
17
|
-
post = JSON.parse(decodedBody) || {};
|
|
18
|
-
}
|
|
19
|
-
catch (e) {
|
|
20
|
-
post = querystring.parse(decodedBody) || {};
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch (e) { }
|
|
24
|
-
if (/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)) {
|
|
25
|
-
reject(new Error("[404] Host cannot be an ip address: " + host));
|
|
26
|
-
}
|
|
27
|
-
return { host, url, method, get, post, cookie, headers, session, lambdaContext };
|
|
28
|
-
};
|
|
29
|
-
export default class Lambder {
|
|
30
|
-
apiPath;
|
|
31
|
-
apiVersion;
|
|
32
|
-
isCorsEnabled;
|
|
33
|
-
publicPath;
|
|
34
|
-
_actionList;
|
|
35
|
-
_hookList;
|
|
36
|
-
globalErrorHandler = null;
|
|
37
|
-
routeFallbackHandler = null;
|
|
38
|
-
apiFallbackHandler = null;
|
|
39
|
-
constructor({ publicPath, apiPath, apiVersion, isCorsEnabled, }) {
|
|
40
|
-
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
41
|
-
this.apiPath = apiPath ?? "/api";
|
|
42
|
-
this.apiVersion = apiVersion ?? null;
|
|
43
|
-
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
44
|
-
this._actionList = [];
|
|
45
|
-
this._hookList = {
|
|
46
|
-
"beforeRender": [],
|
|
47
|
-
"afterRender": [],
|
|
48
|
-
"fallback": [],
|
|
49
|
-
"completed": []
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
setRouteFallbackHandler(routeFallbackHandler) {
|
|
53
|
-
this.routeFallbackHandler = routeFallbackHandler;
|
|
54
|
-
}
|
|
55
|
-
setApiFallbackHandler(apiFallbackHandler) {
|
|
56
|
-
this.apiFallbackHandler = apiFallbackHandler;
|
|
57
|
-
}
|
|
58
|
-
setGlobalErrorHandler(globalErrorHandler) {
|
|
59
|
-
this.globalErrorHandler = globalErrorHandler;
|
|
60
|
-
}
|
|
61
|
-
async validateSession(session) {
|
|
62
|
-
if (!session)
|
|
63
|
-
return false;
|
|
64
|
-
if (!session.userId || !session.userIdHash || !session.sessionHash || !session.csrfToken)
|
|
65
|
-
return false;
|
|
66
|
-
if (!session.createdTimeStamp || !session.expiresTimeStamp)
|
|
67
|
-
return false;
|
|
68
|
-
if (session.expiresTimeStamp > Date.now())
|
|
69
|
-
return false;
|
|
70
|
-
// Check DDB;
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
;
|
|
74
|
-
async handleNoMatchedAction(renderContext, resolver) {
|
|
75
|
-
for (const hook of this._hookList["fallback"]) {
|
|
76
|
-
await hook.hookFn(renderContext, resolver);
|
|
77
|
-
}
|
|
78
|
-
if (renderContext.url === this.apiPath) {
|
|
79
|
-
if (this.apiFallbackHandler) {
|
|
80
|
-
resolver.resolve(await this.apiFallbackHandler(renderContext, resolver));
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
resolver.resolve({ statusCode: 204, body: "API handler not set.", });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
else if (this.routeFallbackHandler) {
|
|
87
|
-
resolver.resolve(await this.routeFallbackHandler(renderContext, resolver));
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
resolver.resolve({ statusCode: 204, body: "Handler not set.", });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
async addModule(moduleFn) {
|
|
94
|
-
await moduleFn(this);
|
|
95
|
-
}
|
|
96
|
-
addRoute(condition, actionFn) {
|
|
97
|
-
this._actionList.push({
|
|
98
|
-
conditionFn: async (renderContext) => (renderContext.method === "GET" &&
|
|
99
|
-
((typeof condition === "string" && renderContext.url === condition) ||
|
|
100
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
101
|
-
(condition?.constructor == RegExp && condition.test(renderContext.url)))),
|
|
102
|
-
actionFn: async (renderContext, resolver) => await actionFn(renderContext, resolver)
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
;
|
|
106
|
-
addSessionRoute(condition, actionFn) {
|
|
107
|
-
this._actionList.push({
|
|
108
|
-
conditionFn: async (renderContext) => (renderContext.method === "GET" &&
|
|
109
|
-
((typeof condition === "string" && renderContext.url === condition) ||
|
|
110
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
111
|
-
(condition?.constructor == RegExp && condition.test(renderContext.url)))),
|
|
112
|
-
actionFn: async (renderContext, resolver) => await actionFn(renderContext, resolver)
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
;
|
|
116
|
-
addApi(condition, actionFn) {
|
|
117
|
-
this._actionList.push({
|
|
118
|
-
conditionFn: async (renderContext) => (renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
119
|
-
((typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
120
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
121
|
-
(condition?.constructor == RegExp && condition.test(renderContext.post?.api)))),
|
|
122
|
-
actionFn: async (renderContext, resolver) => await actionFn(renderContext, resolver)
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
;
|
|
126
|
-
addSessionApi(condition, actionFn) {
|
|
127
|
-
this._actionList.push({
|
|
128
|
-
conditionFn: async (renderContext) => (renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
129
|
-
((typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
130
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
131
|
-
(condition?.constructor == RegExp && condition.test(renderContext.post?.api)))),
|
|
132
|
-
actionFn: async (renderContext, resolver) => {
|
|
133
|
-
const isSessionValid = this.validateSession(renderContext.session);
|
|
134
|
-
if (!isSessionValid)
|
|
135
|
-
throw new Error("Session not found");
|
|
136
|
-
return await actionFn(renderContext, resolver);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
;
|
|
141
|
-
async addHook(hookEvent, hookFn, priority = 0) {
|
|
142
|
-
if (hookEvent === "created") {
|
|
143
|
-
await hookFn(this);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
this._hookList[hookEvent].push({ priority, hookFn });
|
|
147
|
-
this._hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async render(event, lambdaContext) {
|
|
151
|
-
return await new Promise(async (resolve, reject) => {
|
|
152
|
-
const resolver = new Resolver({
|
|
153
|
-
isCorsEnabled: this.isCorsEnabled,
|
|
154
|
-
publicPath: this.publicPath,
|
|
155
|
-
apiVersion: this.apiVersion,
|
|
156
|
-
resolve, reject,
|
|
157
|
-
});
|
|
158
|
-
let renderContext = createContext(event, lambdaContext, resolve, reject);
|
|
159
|
-
if (renderContext.method === "OPTIONS")
|
|
160
|
-
return resolver.cors();
|
|
161
|
-
const firstMatchedAction = await (async () => {
|
|
162
|
-
for (const action of this._actionList) {
|
|
163
|
-
if (await action.conditionFn(renderContext))
|
|
164
|
-
return action;
|
|
165
|
-
}
|
|
166
|
-
})();
|
|
167
|
-
if (firstMatchedAction) {
|
|
168
|
-
for (const hook of this._hookList["beforeRender"]) {
|
|
169
|
-
renderContext = await hook.hookFn(renderContext, resolver);
|
|
170
|
-
}
|
|
171
|
-
let renderResult = await firstMatchedAction.actionFn(renderContext, resolver);
|
|
172
|
-
for (const hook of this._hookList["afterRender"]) {
|
|
173
|
-
renderResult = await hook.hookFn(renderContext, resolver, renderResult);
|
|
174
|
-
}
|
|
175
|
-
resolve(renderResult);
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
return this.handleNoMatchedAction(renderContext, resolver);
|
|
179
|
-
}
|
|
180
|
-
}).then(async (renderResult) => {
|
|
181
|
-
for (const hook of this._hookList["completed"]) {
|
|
182
|
-
renderResult = await hook.hookFn(renderResult);
|
|
183
|
-
}
|
|
184
|
-
return renderResult;
|
|
185
|
-
}).catch(async (err) => {
|
|
186
|
-
if (this.globalErrorHandler) {
|
|
187
|
-
return this.globalErrorHandler(err);
|
|
188
|
-
}
|
|
189
|
-
return { statusCode: 500, body: "Internal Server Error.", };
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
package/package.json
CHANGED
package/src/Lambder.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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.js";
|
|
6
|
+
|
|
7
|
+
type DDBSession = {
|
|
8
|
+
userId: string,
|
|
9
|
+
userIdHash: string;
|
|
10
|
+
sessionHash: string;
|
|
11
|
+
csrfToken: string;
|
|
12
|
+
createdTimeStamp: number;
|
|
13
|
+
expiresTimeStamp: number;
|
|
14
|
+
data: { [key:string]: any };
|
|
15
|
+
} | null;
|
|
16
|
+
|
|
17
|
+
type RenderContext = {
|
|
18
|
+
host: string;
|
|
19
|
+
url: string;
|
|
20
|
+
method: string;
|
|
21
|
+
get: { [key: string]: any };
|
|
22
|
+
post: { [key: string]: any };
|
|
23
|
+
cookie: { [key: string]: any };
|
|
24
|
+
headers: APIGatewayProxyEventHeaders;
|
|
25
|
+
session: DDBSession;
|
|
26
|
+
lambdaContext: Context;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ConditionFunction = (renderContext: RenderContext) => boolean|Promise<boolean>;
|
|
30
|
+
type ActionFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse|Promise<ResolverResponse>;
|
|
31
|
+
type ActionType = { conditionFn: ConditionFunction, actionFn: ActionFunction };
|
|
32
|
+
|
|
33
|
+
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
34
|
+
type HookBeforeRenderFunction = (renderContext: RenderContext, resolver: Resolver) => RenderContext|Promise<RenderContext>;
|
|
35
|
+
type HookAfterRenderFunction = (renderContext: RenderContext, resolver: Resolver, renderResult: ResolverResponse) => ResolverResponse|Promise<ResolverResponse>;
|
|
36
|
+
type HookFallbackFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse|Promise<ResolverResponse>;
|
|
37
|
+
type HookCompletedFunction = (renderResult: ResolverResponse) => ResolverResponse|Promise<ResolverResponse>;
|
|
38
|
+
|
|
39
|
+
type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback"|"completed";
|
|
40
|
+
type HookListType = {
|
|
41
|
+
priority: number,
|
|
42
|
+
hookFn: HookCreatedFunction|HookBeforeRenderFunction|HookAfterRenderFunction|HookFallbackFunction|HookCompletedFunction,
|
|
43
|
+
}[];
|
|
44
|
+
|
|
45
|
+
export const createContext = (
|
|
46
|
+
event: APIGatewayProxyEvent,
|
|
47
|
+
lambdaContext: Context,
|
|
48
|
+
resolve: Function,
|
|
49
|
+
reject: Function
|
|
50
|
+
):RenderContext => {
|
|
51
|
+
const host = event.headers.Host || event.headers.host || "";
|
|
52
|
+
const url = event.path;
|
|
53
|
+
const get: { [key:string]: any } = event.queryStringParameters || {};
|
|
54
|
+
const method = event.httpMethod;
|
|
55
|
+
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
56
|
+
const headers = event.headers;
|
|
57
|
+
let post:{ [key:string]: any } = {};
|
|
58
|
+
const session = null;
|
|
59
|
+
try {
|
|
60
|
+
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
61
|
+
try { post = JSON.parse(decodedBody) || {}; }
|
|
62
|
+
catch(e){ post = querystring.parse(decodedBody) || {}; }
|
|
63
|
+
}catch(e){}
|
|
64
|
+
if(/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)){ reject(new Error("[404] Host cannot be an ip address: " + host)); }
|
|
65
|
+
return { host, url, method, get, post, cookie, headers, session, lambdaContext };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export default class Lambder {
|
|
70
|
+
private apiPath: string;
|
|
71
|
+
private apiVersion: null|string;
|
|
72
|
+
private isCorsEnabled: boolean;
|
|
73
|
+
private publicPath: string;
|
|
74
|
+
|
|
75
|
+
private _actionList: ActionType[];
|
|
76
|
+
private _hookList: {
|
|
77
|
+
"beforeRender": { priority: number, hookFn: HookBeforeRenderFunction }[],
|
|
78
|
+
"afterRender": { priority: number, hookFn: HookAfterRenderFunction }[],
|
|
79
|
+
"fallback": { priority: number, hookFn: HookFallbackFunction }[],
|
|
80
|
+
"completed": { priority: number, hookFn: HookCompletedFunction }[]
|
|
81
|
+
};
|
|
82
|
+
private globalErrorHandler: Function|null = null;
|
|
83
|
+
private routeFallbackHandler: Function|null = null;
|
|
84
|
+
private apiFallbackHandler: Function|null = null;
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
{ publicPath, apiPath, apiVersion, isCorsEnabled, }:
|
|
88
|
+
{ publicPath: string, apiPath?: string, apiVersion?: string, isCorsEnabled?: boolean, }
|
|
89
|
+
){
|
|
90
|
+
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
91
|
+
this.apiPath = apiPath ?? "/api";
|
|
92
|
+
this.apiVersion = apiVersion ?? null;
|
|
93
|
+
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
94
|
+
|
|
95
|
+
this._actionList = [];
|
|
96
|
+
this._hookList = {
|
|
97
|
+
"beforeRender": [],
|
|
98
|
+
"afterRender": [],
|
|
99
|
+
"fallback": [],
|
|
100
|
+
"completed": []
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setRouteFallbackHandler(routeFallbackHandler: (renderContext:RenderContext, resolver: Resolver)=>ResolverResponse){
|
|
105
|
+
this.routeFallbackHandler = routeFallbackHandler;
|
|
106
|
+
}
|
|
107
|
+
setApiFallbackHandler(apiFallbackHandler: (renderContext:RenderContext, resolver: Resolver)=>ResolverResponse){
|
|
108
|
+
this.apiFallbackHandler = apiFallbackHandler;
|
|
109
|
+
}
|
|
110
|
+
setGlobalErrorHandler(globalErrorHandler: (err: Error)=>ResolverResponse){
|
|
111
|
+
this.globalErrorHandler = globalErrorHandler;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async validateSession (session: DDBSession): Promise<boolean>{
|
|
115
|
+
if(!session) return false;
|
|
116
|
+
if(!session.userId || !session.userIdHash || !session.sessionHash || !session.csrfToken) return false;
|
|
117
|
+
if(!session.createdTimeStamp || !session.expiresTimeStamp) return false;
|
|
118
|
+
if(session.expiresTimeStamp > Date.now()) return false;
|
|
119
|
+
// Check DDB;
|
|
120
|
+
return false;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private async handleNoMatchedAction(renderContext: RenderContext, resolver: Resolver){
|
|
124
|
+
for(const hook of this._hookList["fallback"]){
|
|
125
|
+
await hook.hookFn(renderContext, resolver);
|
|
126
|
+
}
|
|
127
|
+
if(renderContext.url === this.apiPath){
|
|
128
|
+
if(this.apiFallbackHandler){
|
|
129
|
+
resolver.resolve(await this.apiFallbackHandler(renderContext, resolver));
|
|
130
|
+
}else{
|
|
131
|
+
resolver.resolve({ statusCode: 204, body: "API handler not set.", })
|
|
132
|
+
}
|
|
133
|
+
}else if(this.routeFallbackHandler){
|
|
134
|
+
resolver.resolve(await this.routeFallbackHandler(renderContext, resolver));
|
|
135
|
+
}else{
|
|
136
|
+
resolver.resolve({ statusCode: 204, body: "Handler not set.", })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async addModule(moduleFn: Function): Promise<void>{
|
|
141
|
+
await moduleFn(this);
|
|
142
|
+
}
|
|
143
|
+
addRoute(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
144
|
+
this._actionList.push({
|
|
145
|
+
conditionFn: async (renderContext:RenderContext) => (
|
|
146
|
+
renderContext.method === "GET" &&
|
|
147
|
+
(
|
|
148
|
+
(typeof condition === "string" && renderContext.url === condition) ||
|
|
149
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
150
|
+
(condition?.constructor == RegExp && condition.test(renderContext.url))
|
|
151
|
+
)
|
|
152
|
+
),
|
|
153
|
+
actionFn: async (renderContext:RenderContext, resolver: Resolver) => await actionFn(renderContext, resolver)
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
addSessionRoute(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
158
|
+
this._actionList.push({
|
|
159
|
+
conditionFn: async (renderContext:RenderContext) => (
|
|
160
|
+
renderContext.method === "GET" &&
|
|
161
|
+
(
|
|
162
|
+
(typeof condition === "string" && renderContext.url === condition) ||
|
|
163
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
164
|
+
(condition?.constructor == RegExp && condition.test(renderContext.url))
|
|
165
|
+
)
|
|
166
|
+
),
|
|
167
|
+
actionFn: async (renderContext:RenderContext, resolver: Resolver) => await actionFn(renderContext, resolver)
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
addApi(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
172
|
+
this._actionList.push({
|
|
173
|
+
conditionFn: async (renderContext:RenderContext) => (
|
|
174
|
+
renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
175
|
+
(
|
|
176
|
+
(typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
177
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
178
|
+
(condition?.constructor == RegExp && condition.test(renderContext.post?.api))
|
|
179
|
+
)
|
|
180
|
+
),
|
|
181
|
+
actionFn: async (renderContext:RenderContext, resolver: Resolver) => await actionFn(renderContext, resolver)
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
addSessionApi(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
186
|
+
this._actionList.push({
|
|
187
|
+
conditionFn: async (renderContext:RenderContext) => (
|
|
188
|
+
renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
189
|
+
(
|
|
190
|
+
(typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
191
|
+
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
192
|
+
(condition?.constructor == RegExp && condition.test(renderContext.post?.api))
|
|
193
|
+
)
|
|
194
|
+
),
|
|
195
|
+
actionFn: async (renderContext:RenderContext, resolver: Resolver) => {
|
|
196
|
+
const isSessionValid = this.validateSession(renderContext.session);
|
|
197
|
+
if(!isSessionValid) throw new Error("Session not found");
|
|
198
|
+
return await actionFn(renderContext, resolver);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
205
|
+
async addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
206
|
+
async addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
207
|
+
async addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
208
|
+
async addHook(hookEvent: 'completed', hookFn: HookCompletedFunction, priority?: number): Promise<void>;
|
|
209
|
+
async addHook(
|
|
210
|
+
hookEvent:HookEventType,
|
|
211
|
+
hookFn: HookCreatedFunction & HookBeforeRenderFunction & HookAfterRenderFunction & HookFallbackFunction & HookCompletedFunction,
|
|
212
|
+
priority = 0
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
if(hookEvent === "created"){
|
|
215
|
+
await hookFn(this);
|
|
216
|
+
}else{
|
|
217
|
+
this._hookList[hookEvent].push({ priority, hookFn });
|
|
218
|
+
this._hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async render(
|
|
224
|
+
event: APIGatewayProxyEvent,
|
|
225
|
+
lambdaContext: Context
|
|
226
|
+
): Promise<ResolverResponse>{
|
|
227
|
+
return await new Promise(async (
|
|
228
|
+
resolve:(renderResult: ResolverResponse)=>void,
|
|
229
|
+
reject:(err: Error)=>void
|
|
230
|
+
)=> {
|
|
231
|
+
const resolver = new Resolver({
|
|
232
|
+
isCorsEnabled: this.isCorsEnabled,
|
|
233
|
+
publicPath: this.publicPath,
|
|
234
|
+
apiVersion: this.apiVersion,
|
|
235
|
+
resolve, reject,
|
|
236
|
+
});
|
|
237
|
+
let renderContext:RenderContext = createContext(event, lambdaContext, resolve, reject)
|
|
238
|
+
if(renderContext.method === "OPTIONS") return resolver.cors();
|
|
239
|
+
|
|
240
|
+
const firstMatchedAction = await (async () => {
|
|
241
|
+
for(const action of this._actionList){
|
|
242
|
+
if(await action.conditionFn(renderContext)) return action;
|
|
243
|
+
}
|
|
244
|
+
})();
|
|
245
|
+
|
|
246
|
+
if(firstMatchedAction){
|
|
247
|
+
for(const hook of this._hookList["beforeRender"]){
|
|
248
|
+
renderContext = await hook.hookFn(renderContext, resolver);
|
|
249
|
+
}
|
|
250
|
+
let renderResult = await firstMatchedAction.actionFn(renderContext, resolver);
|
|
251
|
+
for(const hook of this._hookList["afterRender"]){
|
|
252
|
+
renderResult = await hook.hookFn(renderContext, resolver, renderResult);
|
|
253
|
+
}
|
|
254
|
+
resolve(renderResult);
|
|
255
|
+
}else{
|
|
256
|
+
return this.handleNoMatchedAction(renderContext, resolver);
|
|
257
|
+
}
|
|
258
|
+
}).then(async (renderResult: ResolverResponse):Promise<ResolverResponse> => {
|
|
259
|
+
for(const hook of this._hookList["completed"]){
|
|
260
|
+
renderResult = await hook.hookFn(renderResult);
|
|
261
|
+
}
|
|
262
|
+
return renderResult;
|
|
263
|
+
}).catch(async (err: Error):Promise<ResolverResponse> => {
|
|
264
|
+
if(this.globalErrorHandler){
|
|
265
|
+
return this.globalErrorHandler(err);
|
|
266
|
+
}
|
|
267
|
+
return { statusCode: 500, body: "Internal Server Error.", }
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
}
|
package/src/Resolver.ts
CHANGED
|
@@ -39,7 +39,20 @@ interface IResolverMethods {
|
|
|
39
39
|
cors(): any;
|
|
40
40
|
fileBase64(fileBase64: string, mimeType: string, headers?: { [key: string]: string | string[] }): any;
|
|
41
41
|
file(filePath: string, headers?: { [key: string]: string | string[] }): any;
|
|
42
|
-
api({
|
|
42
|
+
api({
|
|
43
|
+
payload,
|
|
44
|
+
versionExpired, sessionExpired, notAuthorized,
|
|
45
|
+
message, errorMessage,
|
|
46
|
+
...other
|
|
47
|
+
}: {
|
|
48
|
+
payload?: any;
|
|
49
|
+
versionExpired?: boolean;
|
|
50
|
+
sessionExpired?: boolean;
|
|
51
|
+
notAuthorized?: boolean;
|
|
52
|
+
message?: any;
|
|
53
|
+
errorMessage?: any;
|
|
54
|
+
},
|
|
55
|
+
headers?: { [key:string]: string|string[] }): any;
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
|
package/src/index.ts
CHANGED
|
@@ -1,273 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import cookieParser from "cookie";
|
|
3
|
-
|
|
4
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
5
|
-
import Resolver, { ResolverResponse } from "./Resolver.js";
|
|
1
|
+
import Lambder from './Lambder.js';
|
|
6
2
|
|
|
3
|
+
export default Lambder;
|
|
7
4
|
export { default as LambderCaller } from "./LambderCaller.js";
|
|
8
|
-
|
|
9
|
-
type DDBSession = {
|
|
10
|
-
userId: string,
|
|
11
|
-
userIdHash: string;
|
|
12
|
-
sessionHash: string;
|
|
13
|
-
csrfToken: string;
|
|
14
|
-
createdTimeStamp: number;
|
|
15
|
-
expiresTimeStamp: number;
|
|
16
|
-
data: { [key:string]: any };
|
|
17
|
-
} | null;
|
|
18
|
-
|
|
19
|
-
type RenderContext = {
|
|
20
|
-
host: string;
|
|
21
|
-
url: string;
|
|
22
|
-
method: string;
|
|
23
|
-
get: { [key: string]: any };
|
|
24
|
-
post: { [key: string]: any };
|
|
25
|
-
cookie: { [key: string]: any };
|
|
26
|
-
headers: APIGatewayProxyEventHeaders;
|
|
27
|
-
session: DDBSession;
|
|
28
|
-
lambdaContext: Context;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type ConditionFunction = (renderContext: RenderContext) => boolean|Promise<boolean>;
|
|
32
|
-
type ActionFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse|Promise<ResolverResponse>;
|
|
33
|
-
type ActionType = { conditionFn: ConditionFunction, actionFn: ActionFunction };
|
|
34
|
-
|
|
35
|
-
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
36
|
-
type HookBeforeRenderFunction = (renderContext: RenderContext, resolver: Resolver) => RenderContext|Promise<RenderContext>;
|
|
37
|
-
type HookAfterRenderFunction = (renderContext: RenderContext, resolver: Resolver, renderResult: ResolverResponse) => ResolverResponse|Promise<ResolverResponse>;
|
|
38
|
-
type HookFallbackFunction = (renderContext: RenderContext, resolver: Resolver) => ResolverResponse|Promise<ResolverResponse>;
|
|
39
|
-
type HookCompletedFunction = (renderResult: ResolverResponse) => ResolverResponse|Promise<ResolverResponse>;
|
|
40
|
-
|
|
41
|
-
type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback"|"completed";
|
|
42
|
-
type HookListType = {
|
|
43
|
-
priority: number,
|
|
44
|
-
hookFn: HookCreatedFunction|HookBeforeRenderFunction|HookAfterRenderFunction|HookFallbackFunction|HookCompletedFunction,
|
|
45
|
-
}[];
|
|
46
|
-
|
|
47
|
-
export const createContext = (
|
|
48
|
-
event: APIGatewayProxyEvent,
|
|
49
|
-
lambdaContext: Context,
|
|
50
|
-
resolve: Function,
|
|
51
|
-
reject: Function
|
|
52
|
-
):RenderContext => {
|
|
53
|
-
const host = event.headers.Host || event.headers.host || "";
|
|
54
|
-
const url = event.path;
|
|
55
|
-
const get: { [key:string]: any } = event.queryStringParameters || {};
|
|
56
|
-
const method = event.httpMethod;
|
|
57
|
-
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
58
|
-
const headers = event.headers;
|
|
59
|
-
let post:{ [key:string]: any } = {};
|
|
60
|
-
const session = null;
|
|
61
|
-
try {
|
|
62
|
-
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
63
|
-
try { post = JSON.parse(decodedBody) || {}; }
|
|
64
|
-
catch(e){ post = querystring.parse(decodedBody) || {}; }
|
|
65
|
-
}catch(e){}
|
|
66
|
-
if(/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)){ reject(new Error("[404] Host cannot be an ip address: " + host)); }
|
|
67
|
-
return { host, url, method, get, post, cookie, headers, session, lambdaContext };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export default class Lambder {
|
|
72
|
-
private apiPath: string;
|
|
73
|
-
private apiVersion: null|string;
|
|
74
|
-
private isCorsEnabled: boolean;
|
|
75
|
-
private publicPath: string;
|
|
76
|
-
|
|
77
|
-
private _actionList: ActionType[];
|
|
78
|
-
private _hookList: {
|
|
79
|
-
"beforeRender": { priority: number, hookFn: HookBeforeRenderFunction }[],
|
|
80
|
-
"afterRender": { priority: number, hookFn: HookAfterRenderFunction }[],
|
|
81
|
-
"fallback": { priority: number, hookFn: HookFallbackFunction }[],
|
|
82
|
-
"completed": { priority: number, hookFn: HookCompletedFunction }[]
|
|
83
|
-
};
|
|
84
|
-
private globalErrorHandler: Function|null = null;
|
|
85
|
-
private routeFallbackHandler: Function|null = null;
|
|
86
|
-
private apiFallbackHandler: Function|null = null;
|
|
87
|
-
|
|
88
|
-
constructor(
|
|
89
|
-
{ publicPath, apiPath, apiVersion, isCorsEnabled, }:
|
|
90
|
-
{ publicPath: string, apiPath?: string, apiVersion?: string, isCorsEnabled?: boolean, }
|
|
91
|
-
){
|
|
92
|
-
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
93
|
-
this.apiPath = apiPath ?? "/api";
|
|
94
|
-
this.apiVersion = apiVersion ?? null;
|
|
95
|
-
this.isCorsEnabled = isCorsEnabled ?? false;
|
|
96
|
-
|
|
97
|
-
this._actionList = [];
|
|
98
|
-
this._hookList = {
|
|
99
|
-
"beforeRender": [],
|
|
100
|
-
"afterRender": [],
|
|
101
|
-
"fallback": [],
|
|
102
|
-
"completed": []
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
setRouteFallbackHandler(routeFallbackHandler: (renderContext:RenderContext, resolver: Resolver)=>ResolverResponse){
|
|
107
|
-
this.routeFallbackHandler = routeFallbackHandler;
|
|
108
|
-
}
|
|
109
|
-
setApiFallbackHandler(apiFallbackHandler: (renderContext:RenderContext, resolver: Resolver)=>ResolverResponse){
|
|
110
|
-
this.apiFallbackHandler = apiFallbackHandler;
|
|
111
|
-
}
|
|
112
|
-
setGlobalErrorHandler(globalErrorHandler: (err: Error)=>ResolverResponse){
|
|
113
|
-
this.globalErrorHandler = globalErrorHandler;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private async validateSession (session: DDBSession): Promise<boolean>{
|
|
117
|
-
if(!session) return false;
|
|
118
|
-
if(!session.userId || !session.userIdHash || !session.sessionHash || !session.csrfToken) return false;
|
|
119
|
-
if(!session.createdTimeStamp || !session.expiresTimeStamp) return false;
|
|
120
|
-
if(session.expiresTimeStamp > Date.now()) return false;
|
|
121
|
-
// Check DDB;
|
|
122
|
-
return false;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
private async handleNoMatchedAction(renderContext: RenderContext, resolver: Resolver){
|
|
126
|
-
for(const hook of this._hookList["fallback"]){
|
|
127
|
-
await hook.hookFn(renderContext, resolver);
|
|
128
|
-
}
|
|
129
|
-
if(renderContext.url === this.apiPath){
|
|
130
|
-
if(this.apiFallbackHandler){
|
|
131
|
-
resolver.resolve(await this.apiFallbackHandler(renderContext, resolver));
|
|
132
|
-
}else{
|
|
133
|
-
resolver.resolve({ statusCode: 204, body: "API handler not set.", })
|
|
134
|
-
}
|
|
135
|
-
}else if(this.routeFallbackHandler){
|
|
136
|
-
resolver.resolve(await this.routeFallbackHandler(renderContext, resolver));
|
|
137
|
-
}else{
|
|
138
|
-
resolver.resolve({ statusCode: 204, body: "Handler not set.", })
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async addModule(moduleFn: Function): Promise<void>{
|
|
143
|
-
await moduleFn(this);
|
|
144
|
-
}
|
|
145
|
-
addRoute(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
146
|
-
this._actionList.push({
|
|
147
|
-
conditionFn: async (renderContext:RenderContext) => (
|
|
148
|
-
renderContext.method === "GET" &&
|
|
149
|
-
(
|
|
150
|
-
(typeof condition === "string" && renderContext.url === condition) ||
|
|
151
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
152
|
-
(condition?.constructor == RegExp && condition.test(renderContext.url))
|
|
153
|
-
)
|
|
154
|
-
),
|
|
155
|
-
actionFn: async (renderContext:RenderContext, resolver: Resolver) => await actionFn(renderContext, resolver)
|
|
156
|
-
});
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
addSessionRoute(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
160
|
-
this._actionList.push({
|
|
161
|
-
conditionFn: async (renderContext:RenderContext) => (
|
|
162
|
-
renderContext.method === "GET" &&
|
|
163
|
-
(
|
|
164
|
-
(typeof condition === "string" && renderContext.url === condition) ||
|
|
165
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
166
|
-
(condition?.constructor == RegExp && condition.test(renderContext.url))
|
|
167
|
-
)
|
|
168
|
-
),
|
|
169
|
-
actionFn: async (renderContext:RenderContext, resolver: Resolver) => await actionFn(renderContext, resolver)
|
|
170
|
-
});
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
addApi(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
174
|
-
this._actionList.push({
|
|
175
|
-
conditionFn: async (renderContext:RenderContext) => (
|
|
176
|
-
renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
177
|
-
(
|
|
178
|
-
(typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
179
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
180
|
-
(condition?.constructor == RegExp && condition.test(renderContext.post?.api))
|
|
181
|
-
)
|
|
182
|
-
),
|
|
183
|
-
actionFn: async (renderContext:RenderContext, resolver: Resolver) => await actionFn(renderContext, resolver)
|
|
184
|
-
});
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
addSessionApi(condition: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
188
|
-
this._actionList.push({
|
|
189
|
-
conditionFn: async (renderContext:RenderContext) => (
|
|
190
|
-
renderContext.method === "POST" && renderContext.url === this.apiPath &&
|
|
191
|
-
(
|
|
192
|
-
(typeof condition === "string" && renderContext.post?.api === condition) ||
|
|
193
|
-
(typeof condition === "function" && (await condition(renderContext))) ||
|
|
194
|
-
(condition?.constructor == RegExp && condition.test(renderContext.post?.api))
|
|
195
|
-
)
|
|
196
|
-
),
|
|
197
|
-
actionFn: async (renderContext:RenderContext, resolver: Resolver) => {
|
|
198
|
-
const isSessionValid = this.validateSession(renderContext.session);
|
|
199
|
-
if(!isSessionValid) throw new Error("Session not found");
|
|
200
|
-
return await actionFn(renderContext, resolver);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
207
|
-
async addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
208
|
-
async addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
209
|
-
async addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
210
|
-
async addHook(hookEvent: 'completed', hookFn: HookCompletedFunction, priority?: number): Promise<void>;
|
|
211
|
-
async addHook(
|
|
212
|
-
hookEvent:HookEventType,
|
|
213
|
-
hookFn: HookCreatedFunction & HookBeforeRenderFunction & HookAfterRenderFunction & HookFallbackFunction & HookCompletedFunction,
|
|
214
|
-
priority = 0
|
|
215
|
-
): Promise<void> {
|
|
216
|
-
if(hookEvent === "created"){
|
|
217
|
-
await hookFn(this);
|
|
218
|
-
}else{
|
|
219
|
-
this._hookList[hookEvent].push({ priority, hookFn });
|
|
220
|
-
this._hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
async render(
|
|
226
|
-
event: APIGatewayProxyEvent,
|
|
227
|
-
lambdaContext: Context
|
|
228
|
-
): Promise<ResolverResponse>{
|
|
229
|
-
return await new Promise(async (
|
|
230
|
-
resolve:(renderResult: ResolverResponse)=>void,
|
|
231
|
-
reject:(err: Error)=>void
|
|
232
|
-
)=> {
|
|
233
|
-
const resolver = new Resolver({
|
|
234
|
-
isCorsEnabled: this.isCorsEnabled,
|
|
235
|
-
publicPath: this.publicPath,
|
|
236
|
-
apiVersion: this.apiVersion,
|
|
237
|
-
resolve, reject,
|
|
238
|
-
});
|
|
239
|
-
let renderContext:RenderContext = createContext(event, lambdaContext, resolve, reject)
|
|
240
|
-
if(renderContext.method === "OPTIONS") return resolver.cors();
|
|
241
|
-
|
|
242
|
-
const firstMatchedAction = await (async () => {
|
|
243
|
-
for(const action of this._actionList){
|
|
244
|
-
if(await action.conditionFn(renderContext)) return action;
|
|
245
|
-
}
|
|
246
|
-
})();
|
|
247
|
-
|
|
248
|
-
if(firstMatchedAction){
|
|
249
|
-
for(const hook of this._hookList["beforeRender"]){
|
|
250
|
-
renderContext = await hook.hookFn(renderContext, resolver);
|
|
251
|
-
}
|
|
252
|
-
let renderResult = await firstMatchedAction.actionFn(renderContext, resolver);
|
|
253
|
-
for(const hook of this._hookList["afterRender"]){
|
|
254
|
-
renderResult = await hook.hookFn(renderContext, resolver, renderResult);
|
|
255
|
-
}
|
|
256
|
-
resolve(renderResult);
|
|
257
|
-
}else{
|
|
258
|
-
return this.handleNoMatchedAction(renderContext, resolver);
|
|
259
|
-
}
|
|
260
|
-
}).then(async (renderResult: ResolverResponse):Promise<ResolverResponse> => {
|
|
261
|
-
for(const hook of this._hookList["completed"]){
|
|
262
|
-
renderResult = await hook.hookFn(renderResult);
|
|
263
|
-
}
|
|
264
|
-
return renderResult;
|
|
265
|
-
}).catch(async (err: Error):Promise<ResolverResponse> => {
|
|
266
|
-
if(this.globalErrorHandler){
|
|
267
|
-
return this.globalErrorHandler(err);
|
|
268
|
-
}
|
|
269
|
-
return { statusCode: 500, body: "Internal Server Error.", }
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
}
|