lambder 1.0.98 → 1.0.102
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/Readme.md +54 -0
- package/dist/Lambder.js +3 -4
- package/dist/LambderResolver.d.ts +3 -0
- package/dist/LambderResolver.js +17 -0
- package/dist/LambderResponseBuilder.d.ts +9 -3
- package/dist/LambderResponseBuilder.js +60 -9
- package/package.json +3 -1
- package/src/Lambder.ts +3 -2
- package/src/LambderResolver.ts +22 -0
- package/src/LambderResponseBuilder.ts +62 -9
package/Readme.md
CHANGED
|
@@ -32,6 +32,7 @@ const lambder = new Lambder({
|
|
|
32
32
|
apiPath: "/secure",
|
|
33
33
|
isCorsEnabled: true,
|
|
34
34
|
publicPath: path.resolve(`./public`),
|
|
35
|
+
// ejsPath: path.resolve(`./ejs-templates`),
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
// Define a simple api
|
|
@@ -64,6 +65,27 @@ lambder.addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
|
|
|
64
65
|
return res.html("Hello from a function route");
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
// Define a simple route that serves an EJS template file
|
|
69
|
+
lambder.addRoute("/product/:productId", (ctx, res) => {
|
|
70
|
+
const product = await getProduct(ctx.pathParams.productId);
|
|
71
|
+
// Serve the file from ejsPath defined above.
|
|
72
|
+
return await res.ejsFile("productPage.html.ejs", { product });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Serve sitemap using an ejs template.
|
|
76
|
+
lambder.addRoute("/sitemap", (ctx, res) => {
|
|
77
|
+
const templateString = `
|
|
78
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
79
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
80
|
+
<%~
|
|
81
|
+
page.urlList.map(url => `<url><loc>${url}</loc></url>`).join("")
|
|
82
|
+
%>
|
|
83
|
+
</urlset>
|
|
84
|
+
`.trim();
|
|
85
|
+
const urlList = [];
|
|
86
|
+
return await res.ejsTemplate(templateString, { urlList }, { "Content-Type": ["application/xml; charset=utf-8"]});
|
|
87
|
+
});
|
|
88
|
+
|
|
67
89
|
|
|
68
90
|
// Match all other paths and serve static files from publicPath, if not found, serve index.html
|
|
69
91
|
lambder.addRoute("/(.*)", (ctx, res)=>{
|
|
@@ -139,6 +161,29 @@ lambder.addModule(async (lambder: Lambder): Promise<void> => {
|
|
|
139
161
|
});
|
|
140
162
|
```
|
|
141
163
|
|
|
164
|
+
### EJS Templates:
|
|
165
|
+
|
|
166
|
+
EJS templates have the variables `page` and `partial` available:
|
|
167
|
+
|
|
168
|
+
A template is the main file that you call with `await res.ejsFile('template-file')`. Templates will have `page` variable available.
|
|
169
|
+
A partial is included from a template, like `<%- await include('partial/header.html.ejs', partialData) -%>` will have `page` and `partial` variables available.
|
|
170
|
+
|
|
171
|
+
An example template:
|
|
172
|
+
```html
|
|
173
|
+
<div>
|
|
174
|
+
<%- await include('partial/header.html.ejs', partialData) -%>
|
|
175
|
+
<div>Page Variable: <pre><%~ JSON.stringify(page, null, 2) %></pre></div>
|
|
176
|
+
<%- await include('partial/footer.html.ejs', partialData) -%>
|
|
177
|
+
</div>
|
|
178
|
+
```
|
|
179
|
+
An example partial:
|
|
180
|
+
```html
|
|
181
|
+
<div>
|
|
182
|
+
<div>Page Variable: <pre><%~ JSON.stringify(page, null, 2) %></pre></div>
|
|
183
|
+
<div>Partial Variable: <pre><%~ JSON.stringify(partial, null, 2) %></pre></div>
|
|
184
|
+
</div>
|
|
185
|
+
```
|
|
186
|
+
|
|
142
187
|
### Project Structure:
|
|
143
188
|
|
|
144
189
|
Add your imports to index.ts:
|
|
@@ -217,6 +262,13 @@ lambder.addApi("getCompanyName", async (ctx, res) => {
|
|
|
217
262
|
// attempts to serve the fallback file.
|
|
218
263
|
// Returns a JSON error if neither file is found.
|
|
219
264
|
|
|
265
|
+
return await res.ejsFile(filePath, pageData, headers);
|
|
266
|
+
// Renders and serves an ejs file from the server's ejs directory, with optional headers.
|
|
267
|
+
// Returns a JSON error if file is not found.
|
|
268
|
+
|
|
269
|
+
return await res.ejsTemplate(template, pageData, headers);
|
|
270
|
+
// Renders and serves an ejs template string, with optional headers.
|
|
271
|
+
|
|
220
272
|
return res.api(payload, { notAuthorized, message, errorMessage }, headers);
|
|
221
273
|
// This function works together with the LambderCaller from the frontend.
|
|
222
274
|
// Sends a standardized API response including the payload and status
|
|
@@ -237,6 +289,8 @@ lambder.addApi("getCompanyName", async (ctx, res) => {
|
|
|
237
289
|
res.die.cors();
|
|
238
290
|
res.die.fileBase64(fileBase64, mimeType, headers);
|
|
239
291
|
res.die.file(filePath, headers, fallbackFilePath);
|
|
292
|
+
await res.die.ejsFile(filePath, pageData, headers);
|
|
293
|
+
await res.die.ejsTemplate(template, pageData, headers);
|
|
240
294
|
res.die.api(payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage }, headers);
|
|
241
295
|
});
|
|
242
296
|
```
|
package/dist/Lambder.js
CHANGED
|
@@ -11,8 +11,9 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
11
11
|
const method = event.httpMethod;
|
|
12
12
|
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
13
13
|
const headers = event.headers;
|
|
14
|
-
let post = {};
|
|
15
14
|
const session = null;
|
|
15
|
+
// Decode body for the post
|
|
16
|
+
let post = {};
|
|
16
17
|
try {
|
|
17
18
|
const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
|
|
18
19
|
try {
|
|
@@ -23,12 +24,10 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
catch (e) { }
|
|
27
|
+
// Parse api variables
|
|
26
28
|
const isAPICall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
27
29
|
const apiName = isAPICall ? post.apiName : null;
|
|
28
30
|
const apiPayload = isAPICall ? post.payload : null;
|
|
29
|
-
if (/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)) {
|
|
30
|
-
throw new Error("[404] Host cannot be an ip address: " + host);
|
|
31
|
-
}
|
|
32
31
|
return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
|
|
33
32
|
};
|
|
34
33
|
export default class Lambder {
|
|
@@ -10,6 +10,8 @@ interface DieResolverMethods {
|
|
|
10
10
|
cors: MethodType<LambderResponseBuilder, 'cors'>;
|
|
11
11
|
fileBase64: MethodType<LambderResponseBuilder, 'fileBase64'>;
|
|
12
12
|
file: MethodType<LambderResponseBuilder, 'file'>;
|
|
13
|
+
ejsFile: MethodType<LambderResponseBuilder, 'ejsFile'>;
|
|
14
|
+
ejsTemplate: MethodType<LambderResponseBuilder, 'ejsTemplate'>;
|
|
13
15
|
api: MethodType<LambderResponseBuilder, 'api'>;
|
|
14
16
|
}
|
|
15
17
|
export default class LambderResolver extends LambderResponseBuilder {
|
|
@@ -24,5 +26,6 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
24
26
|
reject: (err: Error) => void;
|
|
25
27
|
});
|
|
26
28
|
private autoResolve;
|
|
29
|
+
private autoResolvePromise;
|
|
27
30
|
}
|
|
28
31
|
export {};
|
package/dist/LambderResolver.js
CHANGED
|
@@ -17,6 +17,8 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
17
17
|
cors: this.autoResolve(this.cors),
|
|
18
18
|
fileBase64: this.autoResolve(this.fileBase64),
|
|
19
19
|
file: this.autoResolve(this.file),
|
|
20
|
+
ejsFile: this.autoResolvePromise(this.ejsFile),
|
|
21
|
+
ejsTemplate: this.autoResolvePromise(this.ejsTemplate),
|
|
20
22
|
api: this.autoResolve(this.api),
|
|
21
23
|
};
|
|
22
24
|
}
|
|
@@ -27,5 +29,20 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
27
29
|
return result;
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
autoResolvePromise(method) {
|
|
33
|
+
return (...args) => {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
method.apply(this, args)
|
|
36
|
+
.then(result => {
|
|
37
|
+
this.resolve(result);
|
|
38
|
+
resolve(result);
|
|
39
|
+
})
|
|
40
|
+
.catch(err => {
|
|
41
|
+
this.reject(err);
|
|
42
|
+
reject(err);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
30
47
|
}
|
|
31
48
|
;
|
|
@@ -17,14 +17,18 @@ export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
|
17
17
|
export default class LambderResponseBuilder {
|
|
18
18
|
private isCorsEnabled;
|
|
19
19
|
private publicPath;
|
|
20
|
+
private ejsPath;
|
|
20
21
|
private apiVersion;
|
|
21
|
-
constructor({ isCorsEnabled, publicPath, apiVersion }: {
|
|
22
|
+
constructor({ isCorsEnabled, publicPath, apiVersion, ejsPath }: {
|
|
22
23
|
isCorsEnabled: boolean;
|
|
23
24
|
publicPath: string;
|
|
25
|
+
ejsPath?: string | null;
|
|
24
26
|
apiVersion?: string | null;
|
|
25
27
|
});
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
+
private readPublicFileSync;
|
|
29
|
+
private checkPublicFileExist;
|
|
30
|
+
private readEjsFileSync;
|
|
31
|
+
private checkEjsFileExist;
|
|
28
32
|
raw(param: LambderResolverResponse): LambderResolverResponse;
|
|
29
33
|
json(data: Record<string, any>, headers?: Record<string, string | string[]>): LambderResolverResponse;
|
|
30
34
|
xml(data: string): LambderResolverResponse;
|
|
@@ -34,5 +38,7 @@ export default class LambderResponseBuilder {
|
|
|
34
38
|
cors(): LambderResolverResponse;
|
|
35
39
|
fileBase64(fileBase64: string, mimeType: string, headers?: Record<string, string | string[]>): LambderResolverResponse;
|
|
36
40
|
file(filePath: string, headers?: Record<string, string | string[]>, fallbackFilePath?: string): LambderResolverResponse;
|
|
41
|
+
ejsTemplate(template: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
|
|
42
|
+
ejsFile(filePath: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
|
|
37
43
|
api<T = any>(payload: T | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, }?: LambderApiResponseConfig, headers?: Record<string, string | string[]>): LambderResolverResponse;
|
|
38
44
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import ejs from "ejs";
|
|
3
4
|
import mimeTypeResolver from "mime-types";
|
|
4
5
|
const convertToMultiHeader = (headers) => Object.fromEntries(Object.entries(headers || {}).map(([k, v]) => [k, Array.isArray(v) ? v : [v]]));
|
|
5
6
|
const CORS_HEADERS = convertToMultiHeader({
|
|
@@ -11,33 +12,61 @@ const CORS_HEADERS = convertToMultiHeader({
|
|
|
11
12
|
export default class LambderResponseBuilder {
|
|
12
13
|
isCorsEnabled;
|
|
13
14
|
publicPath;
|
|
15
|
+
ejsPath;
|
|
14
16
|
apiVersion;
|
|
15
|
-
constructor({ isCorsEnabled, publicPath, apiVersion }) {
|
|
17
|
+
constructor({ isCorsEnabled, publicPath, apiVersion, ejsPath }) {
|
|
16
18
|
this.isCorsEnabled = isCorsEnabled;
|
|
17
19
|
this.publicPath = publicPath;
|
|
20
|
+
this.ejsPath = ejsPath ?? null;
|
|
18
21
|
this.apiVersion = apiVersion ?? null;
|
|
19
22
|
}
|
|
20
23
|
;
|
|
21
|
-
|
|
24
|
+
readPublicFileSync(filePath) {
|
|
22
25
|
const publicPath = path.resolve(this.publicPath);
|
|
23
26
|
const absolutePath = path.join(publicPath, filePath);
|
|
24
|
-
console.log("
|
|
27
|
+
console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
|
|
25
28
|
if (!absolutePath.includes(publicPath)) {
|
|
26
|
-
return "
|
|
29
|
+
return "forbidden-public-path";
|
|
27
30
|
}
|
|
28
31
|
return fs.readFileSync(absolutePath);
|
|
29
32
|
}
|
|
30
33
|
;
|
|
31
|
-
|
|
34
|
+
checkPublicFileExist(filePath) {
|
|
32
35
|
const publicPath = path.resolve(this.publicPath);
|
|
33
36
|
const absolutePath = path.join(this.publicPath, filePath);
|
|
34
|
-
console.log("
|
|
37
|
+
console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
|
|
35
38
|
if (!absolutePath.includes(publicPath)) {
|
|
36
39
|
return false;
|
|
37
40
|
}
|
|
38
41
|
return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
|
|
39
42
|
}
|
|
40
43
|
;
|
|
44
|
+
readEjsFileSync(filePath) {
|
|
45
|
+
if (!this.ejsPath) {
|
|
46
|
+
return "EJS PATH NOT SET!";
|
|
47
|
+
}
|
|
48
|
+
const ejsPath = path.resolve(this.ejsPath);
|
|
49
|
+
const absolutePath = path.join(ejsPath, filePath);
|
|
50
|
+
console.log("readEjsFileSync", { filePath, ejsPath, absolutePath });
|
|
51
|
+
if (!absolutePath.includes(ejsPath)) {
|
|
52
|
+
return "forbidden-ejs-path";
|
|
53
|
+
}
|
|
54
|
+
return String(fs.readFileSync(absolutePath));
|
|
55
|
+
}
|
|
56
|
+
;
|
|
57
|
+
checkEjsFileExist(filePath) {
|
|
58
|
+
if (!this.ejsPath) {
|
|
59
|
+
return "EJS PATH NOT SET!";
|
|
60
|
+
}
|
|
61
|
+
const ejsPath = path.resolve(this.ejsPath);
|
|
62
|
+
const absolutePath = path.join(this.ejsPath, filePath);
|
|
63
|
+
console.log("checkEjsFileExist", { filePath, ejsPath, absolutePath });
|
|
64
|
+
if (!absolutePath.includes(ejsPath)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
|
|
68
|
+
}
|
|
69
|
+
;
|
|
41
70
|
raw(param) {
|
|
42
71
|
return param;
|
|
43
72
|
}
|
|
@@ -106,19 +135,41 @@ export default class LambderResponseBuilder {
|
|
|
106
135
|
}
|
|
107
136
|
;
|
|
108
137
|
file(filePath, headers, fallbackFilePath) {
|
|
109
|
-
if (!this.
|
|
110
|
-
if (fallbackFilePath && this.
|
|
138
|
+
if (!this.checkPublicFileExist(filePath)) {
|
|
139
|
+
if (fallbackFilePath && this.checkPublicFileExist(fallbackFilePath)) {
|
|
111
140
|
return this.file(fallbackFilePath, headers);
|
|
112
141
|
}
|
|
113
142
|
return this.json({ error: "File not found: " + filePath });
|
|
114
143
|
}
|
|
115
144
|
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
116
|
-
const body = this.
|
|
145
|
+
const body = this.readPublicFileSync(filePath);
|
|
117
146
|
const bodyBase64 = Buffer.from(body).toString("base64");
|
|
118
147
|
console.log("bodyBase64.length", bodyBase64.length);
|
|
119
148
|
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
120
149
|
}
|
|
121
150
|
;
|
|
151
|
+
async ejsTemplate(template, pageData, headers) {
|
|
152
|
+
const includeRenderedFile = async (filePath, partialData) => {
|
|
153
|
+
const template = await this.readEjsFileSync(filePath);
|
|
154
|
+
return await ejs.render(template, { page: pageData, partial: partialData, include: includeRenderedFile }, { async: true });
|
|
155
|
+
};
|
|
156
|
+
const renderedResult = await ejs.render(template, { page: pageData, include: includeRenderedFile }, { async: true });
|
|
157
|
+
return this.raw({
|
|
158
|
+
statusCode: 200,
|
|
159
|
+
isBase64Encoded: true,
|
|
160
|
+
multiValueHeaders: { "Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers) },
|
|
161
|
+
body: Buffer.from(renderedResult).toString("base64"),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
;
|
|
165
|
+
async ejsFile(filePath, pageData, headers) {
|
|
166
|
+
if (!this.checkEjsFileExist(filePath)) {
|
|
167
|
+
return this.json({ error: "File not found: " + filePath });
|
|
168
|
+
}
|
|
169
|
+
const template = this.readEjsFileSync(filePath);
|
|
170
|
+
return this.ejsTemplate(template, pageData, headers);
|
|
171
|
+
}
|
|
172
|
+
;
|
|
122
173
|
api(payload, { versionExpired, sessionExpired, notAuthorized, message = null, errorMessage = null, } = {
|
|
123
174
|
versionExpired: undefined, sessionExpired: undefined, notAuthorized: undefined,
|
|
124
175
|
message: null, errorMessage: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.102",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"license": "ISC",
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"cookie": "^0.6.0",
|
|
17
|
+
"ejs": "^3.1.9",
|
|
17
18
|
"js-cookie": "^3.0.5",
|
|
18
19
|
"mime-types": "^2.1.35",
|
|
19
20
|
"path-to-regexp": "^6.2.1",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/aws-lambda": "^8.10.136",
|
|
24
25
|
"@types/cookie": "^0.6.0",
|
|
26
|
+
"@types/ejs": "^3.1.5",
|
|
25
27
|
"@types/js-cookie": "^3.0.6",
|
|
26
28
|
"@types/mime-types": "^2.1.4",
|
|
27
29
|
"@types/node": "^20.11.27",
|
package/src/Lambder.ts
CHANGED
|
@@ -63,18 +63,19 @@ export const createContext = (
|
|
|
63
63
|
const method = event.httpMethod;
|
|
64
64
|
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
65
65
|
const headers = event.headers;
|
|
66
|
-
let post: Record<string, any> = {};
|
|
67
66
|
const session = null;
|
|
67
|
+
// Decode body for the post
|
|
68
|
+
let post: Record<string, any> = {};
|
|
68
69
|
try {
|
|
69
70
|
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
70
71
|
try { post = JSON.parse(decodedBody) || {}; }
|
|
71
72
|
catch(e){ post = querystring.parse(decodedBody) || {}; }
|
|
72
73
|
}catch(e){}
|
|
74
|
+
// Parse api variables
|
|
73
75
|
const isAPICall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
74
76
|
const apiName:string = isAPICall ? post.apiName : null;
|
|
75
77
|
const apiPayload:string = isAPICall ? post.payload : null;
|
|
76
78
|
|
|
77
|
-
if(/^(((\d+\.)+\d+)|(([a-f\d]+:)+[a-f\d]+))$/.test(host)){ throw new Error("[404] Host cannot be an ip address: " + host); }
|
|
78
79
|
return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
|
|
79
80
|
}
|
|
80
81
|
|
package/src/LambderResolver.ts
CHANGED
|
@@ -12,6 +12,8 @@ interface DieResolverMethods {
|
|
|
12
12
|
cors: MethodType<LambderResponseBuilder, 'cors'>;
|
|
13
13
|
fileBase64: MethodType<LambderResponseBuilder, 'fileBase64'>;
|
|
14
14
|
file: MethodType<LambderResponseBuilder, 'file'>;
|
|
15
|
+
ejsFile: MethodType<LambderResponseBuilder, 'ejsFile'>;
|
|
16
|
+
ejsTemplate: MethodType<LambderResponseBuilder, 'ejsTemplate'>;
|
|
15
17
|
api: MethodType<LambderResponseBuilder, 'api'>;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -44,6 +46,8 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
44
46
|
cors: this.autoResolve(this.cors),
|
|
45
47
|
fileBase64: this.autoResolve(this.fileBase64),
|
|
46
48
|
file: this.autoResolve(this.file),
|
|
49
|
+
ejsFile: this.autoResolvePromise(this.ejsFile),
|
|
50
|
+
ejsTemplate: this.autoResolvePromise(this.ejsTemplate),
|
|
47
51
|
api: this.autoResolve(this.api),
|
|
48
52
|
};
|
|
49
53
|
}
|
|
@@ -58,4 +62,22 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
private autoResolvePromise<
|
|
66
|
+
T extends (...args: any[]) => Promise<LambderResolverResponse>
|
|
67
|
+
>(method: T): (...funcArgs: Parameters<T>) => Promise<LambderResolverResponse> {
|
|
68
|
+
return (...args: Parameters<T>): Promise<LambderResolverResponse> => {
|
|
69
|
+
return new Promise<LambderResolverResponse>((resolve, reject) => {
|
|
70
|
+
method.apply(this, args)
|
|
71
|
+
.then(result => {
|
|
72
|
+
this.resolve(result);
|
|
73
|
+
resolve(result);
|
|
74
|
+
})
|
|
75
|
+
.catch(err => {
|
|
76
|
+
this.reject(err);
|
|
77
|
+
reject(err);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import ejs from "ejs";
|
|
3
4
|
import mimeTypeResolver from "mime-types";
|
|
4
5
|
|
|
5
6
|
const convertToMultiHeader = (
|
|
@@ -37,37 +38,58 @@ export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
|
37
38
|
export default class LambderResponseBuilder {
|
|
38
39
|
private isCorsEnabled: boolean;
|
|
39
40
|
private publicPath: string;
|
|
41
|
+
private ejsPath: string|null;
|
|
40
42
|
private apiVersion: string|null;
|
|
41
43
|
|
|
42
44
|
constructor(
|
|
43
|
-
{ isCorsEnabled, publicPath, apiVersion }:
|
|
45
|
+
{ isCorsEnabled, publicPath, apiVersion, ejsPath }:
|
|
44
46
|
{
|
|
45
47
|
isCorsEnabled: boolean,
|
|
46
48
|
publicPath: string,
|
|
49
|
+
ejsPath?: string|null,
|
|
47
50
|
apiVersion?: string|null,
|
|
48
51
|
}
|
|
49
52
|
){
|
|
50
53
|
this.isCorsEnabled = isCorsEnabled;
|
|
51
54
|
this.publicPath = publicPath;
|
|
55
|
+
this.ejsPath = ejsPath ?? null;
|
|
52
56
|
this.apiVersion = apiVersion ?? null;
|
|
53
57
|
};
|
|
54
58
|
|
|
55
|
-
private
|
|
59
|
+
private readPublicFileSync(filePath: string){
|
|
56
60
|
const publicPath = path.resolve(this.publicPath);
|
|
57
61
|
const absolutePath = path.join(publicPath, filePath);
|
|
58
|
-
console.log("
|
|
59
|
-
if(!absolutePath.includes(publicPath)){ return "
|
|
62
|
+
console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
|
|
63
|
+
if(!absolutePath.includes(publicPath)){ return "forbidden-public-path"; }
|
|
60
64
|
return fs.readFileSync(absolutePath);
|
|
61
65
|
};
|
|
62
66
|
|
|
63
|
-
private
|
|
67
|
+
private checkPublicFileExist(filePath: string){
|
|
64
68
|
const publicPath = path.resolve(this.publicPath);
|
|
65
69
|
const absolutePath = path.join(this.publicPath, filePath);
|
|
66
|
-
console.log("
|
|
70
|
+
console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
|
|
67
71
|
if(!absolutePath.includes(publicPath)){ return false; }
|
|
68
72
|
return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
|
|
69
73
|
};
|
|
70
74
|
|
|
75
|
+
private readEjsFileSync(filePath: string){
|
|
76
|
+
if(!this.ejsPath){ return "EJS PATH NOT SET!"; }
|
|
77
|
+
const ejsPath = path.resolve(this.ejsPath);
|
|
78
|
+
const absolutePath = path.join(ejsPath, filePath);
|
|
79
|
+
console.log("readEjsFileSync", { filePath, ejsPath, absolutePath });
|
|
80
|
+
if(!absolutePath.includes(ejsPath)){ return "forbidden-ejs-path"; }
|
|
81
|
+
return String(fs.readFileSync(absolutePath));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
private checkEjsFileExist(filePath: string){
|
|
85
|
+
if(!this.ejsPath){ return "EJS PATH NOT SET!"; }
|
|
86
|
+
const ejsPath = path.resolve(this.ejsPath);
|
|
87
|
+
const absolutePath = path.join(this.ejsPath, filePath);
|
|
88
|
+
console.log("checkEjsFileExist", { filePath, ejsPath, absolutePath });
|
|
89
|
+
if(!absolutePath.includes(ejsPath)){ return false; }
|
|
90
|
+
return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
|
|
91
|
+
};
|
|
92
|
+
|
|
71
93
|
raw(param: LambderResolverResponse){
|
|
72
94
|
return param;
|
|
73
95
|
};
|
|
@@ -157,19 +179,50 @@ export default class LambderResponseBuilder {
|
|
|
157
179
|
headers?: Record<string, string|string[]>,
|
|
158
180
|
fallbackFilePath?: string,
|
|
159
181
|
):LambderResolverResponse{
|
|
160
|
-
if(!this.
|
|
161
|
-
if(fallbackFilePath && this.
|
|
182
|
+
if(!this.checkPublicFileExist(filePath)){
|
|
183
|
+
if(fallbackFilePath && this.checkPublicFileExist(fallbackFilePath)){
|
|
162
184
|
return this.file(fallbackFilePath, headers);
|
|
163
185
|
}
|
|
164
186
|
return this.json({ error: "File not found: " + filePath })
|
|
165
187
|
}
|
|
166
188
|
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
167
|
-
const body = this.
|
|
189
|
+
const body = this.readPublicFileSync(filePath);
|
|
168
190
|
const bodyBase64 = Buffer.from(body).toString("base64");
|
|
169
191
|
console.log("bodyBase64.length",bodyBase64.length);
|
|
170
192
|
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
171
193
|
};
|
|
172
194
|
|
|
195
|
+
async ejsTemplate(
|
|
196
|
+
template: string,
|
|
197
|
+
pageData: Record<string, any>,
|
|
198
|
+
headers?: Record<string, string|string[]>,
|
|
199
|
+
):Promise<LambderResolverResponse>{
|
|
200
|
+
const includeRenderedFile = async (filePath: string, partialData: Record<string, any>) => {
|
|
201
|
+
const template = await this.readEjsFileSync(filePath);
|
|
202
|
+
return await ejs.render(template, { page: pageData, partial: partialData, include: includeRenderedFile }, { async: true });
|
|
203
|
+
}
|
|
204
|
+
const renderedResult = await ejs.render(template, { page: pageData, include: includeRenderedFile }, { async: true });
|
|
205
|
+
|
|
206
|
+
return this.raw({
|
|
207
|
+
statusCode: 200,
|
|
208
|
+
isBase64Encoded: true,
|
|
209
|
+
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
210
|
+
body: Buffer.from(renderedResult).toString("base64"),
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
async ejsFile(
|
|
215
|
+
filePath: string,
|
|
216
|
+
pageData: Record<string, any>,
|
|
217
|
+
headers?: Record<string, string|string[]>,
|
|
218
|
+
):Promise<LambderResolverResponse>{
|
|
219
|
+
if(!this.checkEjsFileExist(filePath)){
|
|
220
|
+
return this.json({ error: "File not found: " + filePath })
|
|
221
|
+
}
|
|
222
|
+
const template = this.readEjsFileSync(filePath);
|
|
223
|
+
return this.ejsTemplate(template, pageData, headers);
|
|
224
|
+
};
|
|
225
|
+
|
|
173
226
|
api<T=any>(
|
|
174
227
|
payload: T | null,
|
|
175
228
|
{
|