lambder 1.0.99 → 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 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
  ```
@@ -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 {};
@@ -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 readFileSync;
27
- private checkFileExist;
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
- readFileSync(filePath) {
24
+ readPublicFileSync(filePath) {
22
25
  const publicPath = path.resolve(this.publicPath);
23
26
  const absolutePath = path.join(publicPath, filePath);
24
- console.log("readFileSync", { filePath, publicPath, absolutePath });
27
+ console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
25
28
  if (!absolutePath.includes(publicPath)) {
26
- return "forbiddenpath";
29
+ return "forbidden-public-path";
27
30
  }
28
31
  return fs.readFileSync(absolutePath);
29
32
  }
30
33
  ;
31
- checkFileExist(filePath) {
34
+ checkPublicFileExist(filePath) {
32
35
  const publicPath = path.resolve(this.publicPath);
33
36
  const absolutePath = path.join(this.publicPath, filePath);
34
- console.log("checkFileExist", { filePath, publicPath, absolutePath });
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.checkFileExist(filePath)) {
110
- if (fallbackFilePath && this.checkFileExist(fallbackFilePath)) {
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.readFileSync(filePath);
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.99",
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",
@@ -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 readFileSync(filePath: string){
59
+ private readPublicFileSync(filePath: string){
56
60
  const publicPath = path.resolve(this.publicPath);
57
61
  const absolutePath = path.join(publicPath, filePath);
58
- console.log("readFileSync", { filePath, publicPath, absolutePath });
59
- if(!absolutePath.includes(publicPath)){ return "forbiddenpath"; }
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 checkFileExist(filePath: string){
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("checkFileExist", { filePath, publicPath, absolutePath });
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.checkFileExist(filePath)){
161
- if(fallbackFilePath && this.checkFileExist(fallbackFilePath)){
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.readFileSync(filePath);
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
  {