lambder 2.0.18 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +162 -41
- package/dist/Lambder.d.ts +154 -46
- package/dist/Lambder.js +312 -166
- package/dist/LambderCaller.js +6 -3
- package/dist/LambderContext.d.ts +20 -9
- package/dist/LambderContext.js +57 -17
- package/dist/LambderCors.d.ts +12 -0
- package/dist/LambderCors.js +30 -0
- package/dist/LambderHtml.d.ts +33 -0
- package/dist/LambderHtml.js +62 -0
- package/dist/LambderMSW.d.ts +16 -1
- package/dist/LambderMSW.js +5 -9
- package/dist/LambderPublicFiles.d.ts +47 -0
- package/dist/LambderPublicFiles.js +108 -0
- package/dist/LambderResolver.d.ts +30 -31
- package/dist/LambderResolver.js +29 -43
- package/dist/LambderResponse.d.ts +71 -0
- package/dist/LambderResponse.js +196 -0
- package/dist/LambderResponseBuilder.d.ts +58 -33
- package/dist/LambderResponseBuilder.js +114 -167
- package/dist/LambderRouting.d.ts +23 -0
- package/dist/LambderRouting.js +67 -0
- package/dist/LambderSessionController.d.ts +13 -1
- package/dist/LambderSessionController.js +33 -10
- package/dist/LambderSessionManager.d.ts +3 -1
- package/dist/LambderSessionManager.js +15 -6
- package/dist/LambderTemplatingEngine.d.ts +87 -0
- package/dist/LambderTemplatingEngine.js +156 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +10 -1
- package/dist/node-polyfills.d.ts +4 -2
- package/dist/node-polyfills.js +28 -0
- package/package.json +7 -5
- package/.eslintrc.cjs +0 -26
- package/.vscode/settings.json +0 -26
- package/deploy +0 -22
- package/dist/LambderUtils.d.ts +0 -10
- package/dist/LambderUtils.js +0 -70
- package/docs/DYNAMODB_SETUP.md +0 -96
- package/docs/LAMBDER_MSW.md +0 -409
- package/docs/TYPE_SAFE_QUICK_START.md +0 -77
- package/examples/msw-testing-example.ts +0 -280
- package/examples/secure-session-example.ts +0 -207
- package/examples/zod-chained-api-example.ts +0 -63
- package/src/Lambder.ts +0 -430
- package/src/LambderApiContract.ts +0 -20
- package/src/LambderCaller.ts +0 -238
- package/src/LambderContext.ts +0 -78
- package/src/LambderMSW.ts +0 -180
- package/src/LambderResolver.ts +0 -101
- package/src/LambderResponseBuilder.ts +0 -332
- package/src/LambderSessionController.ts +0 -114
- package/src/LambderSessionManager.ts +0 -217
- package/src/LambderUtils.ts +0 -75
- package/src/index.ts +0 -17
- package/src/node-polyfills.ts +0 -27
- package/tests/error-handling.test.ts +0 -585
- package/tests/file-serving.test.ts +0 -194
- package/tests/fixtures/public/index.html +0 -1
- package/tests/fixtures/public/main.css +0 -1
- package/tests/hooks.test.ts +0 -561
- package/tests/output-type-runtime.test.ts +0 -381
- package/tests/redirect.test.ts +0 -88
- package/tests/routes.test.ts +0 -543
- package/tests/session.test.ts +0 -1083
- package/tests/use-plugin.test.ts +0 -460
- package/tsconfig.json +0 -24
package/src/LambderContext.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import cookieParser from "cookie";
|
|
2
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
3
|
-
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
|
-
|
|
5
|
-
export type LambderRenderContext<TApiPayload = any> = {
|
|
6
|
-
host: string;
|
|
7
|
-
path: string;
|
|
8
|
-
pathParams: Record<string, any> | null;
|
|
9
|
-
method: string;
|
|
10
|
-
get: Record<string, any>;
|
|
11
|
-
post: Record<string, any>;
|
|
12
|
-
cookie: Record<string, any>;
|
|
13
|
-
session: null;
|
|
14
|
-
apiName: string;
|
|
15
|
-
apiPayload: TApiPayload;
|
|
16
|
-
headers: APIGatewayProxyEventHeaders;
|
|
17
|
-
event: APIGatewayProxyEvent;
|
|
18
|
-
lambdaContext: Context;
|
|
19
|
-
_otherInternal: {
|
|
20
|
-
isApiCall: boolean,
|
|
21
|
-
requestVersion: string|null;
|
|
22
|
-
setHeaderFnAccumulator: { key:string, value:string|string[] }[];
|
|
23
|
-
addHeaderFnAccumulator: { key:string, value:string }[];
|
|
24
|
-
logToApiResponseAccumulator: any[];
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type LambderSessionRenderContext<
|
|
29
|
-
TApiPayload = any, SessionData = any
|
|
30
|
-
> = Omit<LambderRenderContext<TApiPayload>, 'session'> & { session: LambderSessionContext<SessionData> };
|
|
31
|
-
|
|
32
|
-
export const createContext = (
|
|
33
|
-
event: APIGatewayProxyEvent,
|
|
34
|
-
lambdaContext: Context,
|
|
35
|
-
apiPath: string,
|
|
36
|
-
):LambderRenderContext<any> => {
|
|
37
|
-
const host = event.headers.Host || event.headers.host || "";
|
|
38
|
-
const path = event.path;
|
|
39
|
-
const pathParams = null;
|
|
40
|
-
const get: Record<string, any> = event.queryStringParameters || {};
|
|
41
|
-
const method = event.httpMethod;
|
|
42
|
-
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
43
|
-
const headers = event.headers;
|
|
44
|
-
|
|
45
|
-
// Decode body for the post
|
|
46
|
-
let post: Record<string, any> = {};
|
|
47
|
-
try {
|
|
48
|
-
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
49
|
-
try { post = JSON.parse(decodedBody) || {}; }
|
|
50
|
-
catch(e){
|
|
51
|
-
const params = new URLSearchParams(decodedBody);
|
|
52
|
-
post = {};
|
|
53
|
-
for(const [key, value] of params.entries()){
|
|
54
|
-
post[key] = value;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}catch(e){}
|
|
58
|
-
// Parse api variables
|
|
59
|
-
|
|
60
|
-
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
61
|
-
const apiName:string = isApiCall ? post.apiName : null;
|
|
62
|
-
const apiPayload:string = isApiCall ? post.payload : null;
|
|
63
|
-
const requestVersion:string = isApiCall ? post.version : null;
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
host, path, pathParams, method,
|
|
67
|
-
get, post, cookie, event,
|
|
68
|
-
session: null,
|
|
69
|
-
apiName, apiPayload,
|
|
70
|
-
headers, lambdaContext,
|
|
71
|
-
_otherInternal: {
|
|
72
|
-
isApiCall, requestVersion,
|
|
73
|
-
setHeaderFnAccumulator: [],
|
|
74
|
-
addHeaderFnAccumulator: [],
|
|
75
|
-
logToApiResponseAccumulator: [],
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
}
|
package/src/LambderMSW.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import type { ApiContractShape } from './LambderApiContract';
|
|
2
|
-
import type { LambderApiResponse } from './LambderResponseBuilder';
|
|
3
|
-
|
|
4
|
-
// MSW types - these will be resolved at runtime when msw is installed
|
|
5
|
-
type RequestHandler = any;
|
|
6
|
-
|
|
7
|
-
type MockApiOptions = {
|
|
8
|
-
versionExpired?: boolean;
|
|
9
|
-
sessionExpired?: boolean;
|
|
10
|
-
notAuthorized?: boolean;
|
|
11
|
-
message?: any;
|
|
12
|
-
errorMessage?: any;
|
|
13
|
-
logList?: any[];
|
|
14
|
-
delay?: number; // Add artificial delay to simulate network latency
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Extended response type that includes apiVersion
|
|
18
|
-
type MockApiResponse<T> = {
|
|
19
|
-
apiVersion?: string;
|
|
20
|
-
} & LambderApiResponse<T>;
|
|
21
|
-
|
|
22
|
-
export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
23
|
-
private apiPath: string;
|
|
24
|
-
private apiVersion?: string;
|
|
25
|
-
private http: any;
|
|
26
|
-
private HttpResponse: any;
|
|
27
|
-
|
|
28
|
-
constructor({
|
|
29
|
-
apiPath,
|
|
30
|
-
apiVersion,
|
|
31
|
-
}: {
|
|
32
|
-
apiPath: string;
|
|
33
|
-
apiVersion?: string;
|
|
34
|
-
}) {
|
|
35
|
-
this.apiPath = apiPath;
|
|
36
|
-
this.apiVersion = apiVersion;
|
|
37
|
-
|
|
38
|
-
// Dynamically import MSW - it needs to be installed by the user
|
|
39
|
-
try {
|
|
40
|
-
const msw = require('msw');
|
|
41
|
-
this.http = msw.http;
|
|
42
|
-
this.HttpResponse = msw.HttpResponse;
|
|
43
|
-
} catch (err) {
|
|
44
|
-
throw new Error('MSW (Mock Service Worker) is required. Install it with: npm install msw --save-dev');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Mock an API endpoint with MSW
|
|
50
|
-
* @param apiName - The name of the API to mock
|
|
51
|
-
* @param handler - Function that returns the mock payload
|
|
52
|
-
* @param options - Additional response options (session expired, version expired, etc.)
|
|
53
|
-
*
|
|
54
|
-
* Note: TypeScript will check that the return type is assignable to the output type,
|
|
55
|
-
* but due to structural typing, extra properties are allowed. Enable strict checks
|
|
56
|
-
* in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
|
|
57
|
-
* for better type safety.
|
|
58
|
-
*/
|
|
59
|
-
mockApi<TApiName extends keyof TContract>(
|
|
60
|
-
apiName: TApiName,
|
|
61
|
-
handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
|
|
62
|
-
options?: MockApiOptions
|
|
63
|
-
): RequestHandler {
|
|
64
|
-
return this.http.post(this.apiPath, async ({ request }: any) => {
|
|
65
|
-
let body: any;
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const clonedRequest = request.clone();
|
|
69
|
-
body = await clonedRequest.json();
|
|
70
|
-
} catch (parseErr) {
|
|
71
|
-
// If JSON parsing fails, return undefined to let other handlers try
|
|
72
|
-
console.warn("LambderMSW: Failed to parse request body as JSON");
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Check if body is valid and has apiName
|
|
77
|
-
if (!body || typeof body.apiName !== 'string') { return; }
|
|
78
|
-
if (body.apiName !== apiName) { return; }
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
// Add artificial delay if specified
|
|
82
|
-
if (options?.delay) {
|
|
83
|
-
await new Promise(resolve => setTimeout(resolve, options.delay));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Call the handler with the payload from the request
|
|
87
|
-
const payload = await handler(body.payload as TContract[TApiName]['input']);
|
|
88
|
-
|
|
89
|
-
const response: MockApiResponse<TContract[TApiName]['output']> = {
|
|
90
|
-
apiVersion: this.apiVersion,
|
|
91
|
-
payload,
|
|
92
|
-
...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
|
|
93
|
-
...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
|
|
94
|
-
...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
|
|
95
|
-
...(options?.message ? { message: options.message } : {}),
|
|
96
|
-
...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
|
|
97
|
-
...(options?.logList?.length ? { logList: options.logList } : {}),
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return this.HttpResponse.json(response);
|
|
101
|
-
} catch (err: any) {
|
|
102
|
-
// Only handle errors that occur during handler execution for matched APIs
|
|
103
|
-
console.error("Error in LambderMSW handler for", apiName, ":", err);
|
|
104
|
-
|
|
105
|
-
const errorResponse: MockApiResponse<null> = {
|
|
106
|
-
apiVersion: this.apiVersion,
|
|
107
|
-
payload: null,
|
|
108
|
-
errorMessage: err.message || "Unknown error",
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return this.HttpResponse.json(errorResponse, { status: 500 });
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Mock an API endpoint that returns a session expired error
|
|
118
|
-
*/
|
|
119
|
-
mockSessionExpired<TApiName extends keyof TContract>(
|
|
120
|
-
apiName: TApiName
|
|
121
|
-
): RequestHandler {
|
|
122
|
-
return this.mockApi(
|
|
123
|
-
apiName,
|
|
124
|
-
async () => null as any,
|
|
125
|
-
{ sessionExpired: true }
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Mock an API endpoint that returns a version expired error
|
|
131
|
-
*/
|
|
132
|
-
mockVersionExpired<TApiName extends keyof TContract>(
|
|
133
|
-
apiName: TApiName
|
|
134
|
-
): RequestHandler {
|
|
135
|
-
return this.mockApi(
|
|
136
|
-
apiName,
|
|
137
|
-
async () => null as any,
|
|
138
|
-
{ versionExpired: true }
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Mock an API endpoint that returns a not authorized error
|
|
144
|
-
*/
|
|
145
|
-
mockNotAuthorized<TApiName extends keyof TContract>(
|
|
146
|
-
apiName: TApiName
|
|
147
|
-
): RequestHandler {
|
|
148
|
-
return this.mockApi(
|
|
149
|
-
apiName,
|
|
150
|
-
async () => null as any,
|
|
151
|
-
{ notAuthorized: true }
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Mock an API endpoint that returns an error message
|
|
157
|
-
*/
|
|
158
|
-
mockError<TApiName extends keyof TContract>(
|
|
159
|
-
apiName: TApiName,
|
|
160
|
-
errorMessage: string
|
|
161
|
-
): RequestHandler {
|
|
162
|
-
return this.mockApi(
|
|
163
|
-
apiName,
|
|
164
|
-
async () => null as any,
|
|
165
|
-
{ errorMessage }
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Mock an API endpoint with a custom message
|
|
171
|
-
*/
|
|
172
|
-
mockWithMessage<TApiName extends keyof TContract>(
|
|
173
|
-
apiName: TApiName,
|
|
174
|
-
handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
|
|
175
|
-
message: any
|
|
176
|
-
): RequestHandler {
|
|
177
|
-
return this.mockApi(apiName, handler, { message });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
package/src/LambderResolver.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
-
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
3
|
-
import LambderUtils from "./LambderUtils.js";
|
|
4
|
-
|
|
5
|
-
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
6
|
-
|
|
7
|
-
interface DieResolverMethods<TOutput> {
|
|
8
|
-
raw: MethodType<LambderResponseBuilder, 'raw'>;
|
|
9
|
-
json: MethodType<LambderResponseBuilder, 'json'>;
|
|
10
|
-
xml: MethodType<LambderResponseBuilder, 'xml'>;
|
|
11
|
-
html: MethodType<LambderResponseBuilder, 'html'>;
|
|
12
|
-
redirect: MethodType<LambderResponseBuilder, 'redirect'>;
|
|
13
|
-
status404: MethodType<LambderResponseBuilder, 'status404'>;
|
|
14
|
-
cors: MethodType<LambderResponseBuilder, 'cors'>;
|
|
15
|
-
fileBase64: MethodType<LambderResponseBuilder, 'fileBase64'>;
|
|
16
|
-
file: MethodType<LambderResponseBuilder, 'file'>;
|
|
17
|
-
ejsFile: MethodType<LambderResponseBuilder, 'ejsFile'>;
|
|
18
|
-
ejsTemplate: MethodType<LambderResponseBuilder, 'ejsTemplate'>;
|
|
19
|
-
api: (
|
|
20
|
-
payload: TOutput | null,
|
|
21
|
-
config?: Parameters<LambderResponseBuilder['api']>[1],
|
|
22
|
-
headers?: Parameters<LambderResponseBuilder['api']>[2]
|
|
23
|
-
) => LambderResolverResponse;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default class LambderResolver<TOutput = any> extends LambderResponseBuilder<TOutput> {
|
|
27
|
-
public resolve: (response: LambderResolverResponse) => void;
|
|
28
|
-
public reject: (err: Error) => void;
|
|
29
|
-
public die: DieResolverMethods<TOutput>;
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
{ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, resolve, reject }:
|
|
33
|
-
{
|
|
34
|
-
isCorsEnabled: boolean,
|
|
35
|
-
publicPath: string,
|
|
36
|
-
apiVersion?: string|null,
|
|
37
|
-
lambderUtils: LambderUtils,
|
|
38
|
-
ctx: LambderRenderContext<any>,
|
|
39
|
-
resolve: (response: LambderResolverResponse) => void,
|
|
40
|
-
reject: (err: Error) => void,
|
|
41
|
-
}
|
|
42
|
-
){
|
|
43
|
-
super({ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, });
|
|
44
|
-
this.resolve = resolve;
|
|
45
|
-
this.reject = reject;
|
|
46
|
-
|
|
47
|
-
this.die = {
|
|
48
|
-
raw: this.autoResolve(this.raw),
|
|
49
|
-
json: this.autoResolve(this.json),
|
|
50
|
-
xml: this.autoResolve(this.xml),
|
|
51
|
-
html: this.autoResolve(this.html),
|
|
52
|
-
redirect: this.autoResolve(this.redirect),
|
|
53
|
-
status404: this.autoResolve(this.status404),
|
|
54
|
-
cors: this.autoResolve(this.cors),
|
|
55
|
-
fileBase64: this.autoResolve(this.fileBase64),
|
|
56
|
-
file: this.autoResolvePromise(this.file),
|
|
57
|
-
ejsFile: this.autoResolvePromise(this.ejsFile),
|
|
58
|
-
ejsTemplate: this.autoResolvePromise(this.ejsTemplate),
|
|
59
|
-
api: this.autoResolve(this.api),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Override api method with proper typing
|
|
64
|
-
api(
|
|
65
|
-
payload: TOutput | null,
|
|
66
|
-
config?: Parameters<LambderResponseBuilder['api']>[1],
|
|
67
|
-
headers?: Parameters<LambderResponseBuilder['api']>[2]
|
|
68
|
-
): LambderResolverResponse {
|
|
69
|
-
return super.api(payload, config, headers);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
private autoResolve<
|
|
74
|
-
T extends (...args: any[]) => LambderResolverResponse
|
|
75
|
-
>(method: T): (...funcArgs: Parameters<T>) => LambderResolverResponse {
|
|
76
|
-
return (...args: Parameters<T>): LambderResolverResponse => {
|
|
77
|
-
const result: LambderResolverResponse = method.apply(this, args);
|
|
78
|
-
this.resolve(result);
|
|
79
|
-
return result;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private autoResolvePromise<
|
|
84
|
-
T extends (...args: any[]) => Promise<LambderResolverResponse>
|
|
85
|
-
>(method: T): (...funcArgs: Parameters<T>) => Promise<LambderResolverResponse> {
|
|
86
|
-
return (...args: Parameters<T>): Promise<LambderResolverResponse> => {
|
|
87
|
-
return new Promise<LambderResolverResponse>((resolve, reject) => {
|
|
88
|
-
method.apply(this, args)
|
|
89
|
-
.then(result => {
|
|
90
|
-
this.resolve(result);
|
|
91
|
-
resolve(result);
|
|
92
|
-
})
|
|
93
|
-
.catch(err => {
|
|
94
|
-
this.reject(err);
|
|
95
|
-
reject(err);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
};
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import mimeTypeResolver from "mime-types";
|
|
2
|
-
import LambderUtils from "./LambderUtils.js";
|
|
3
|
-
import { LambderRenderContext } from "./LambderContext.js";
|
|
4
|
-
import { getFS, getPath } from "./node-polyfills.js";
|
|
5
|
-
|
|
6
|
-
const convertToMultiHeader = (
|
|
7
|
-
headers: Record<string, string|string[]> | undefined
|
|
8
|
-
):Record<string, string[]> =>
|
|
9
|
-
Object.fromEntries(Object.entries(headers||{}).map(([k,v])=>[ k, Array.isArray(v) ? v : [v] ]));
|
|
10
|
-
|
|
11
|
-
const CORS_HEADERS = convertToMultiHeader({
|
|
12
|
-
"Access-Control-Allow-Credentials": "true",
|
|
13
|
-
"Access-Control-Allow-Headers" : "Origin, X-Requested-With, Content-Type, Accept",
|
|
14
|
-
"Access-Control-Allow-Origin": "*",
|
|
15
|
-
"Access-Control-Allow-Methods": "OPTIONS,POST",
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export type HttpStatusCode =
|
|
19
|
-
| 200 | 201 | 202 | 204
|
|
20
|
-
| 300 | 301 | 302 | 303 | 304 | 307 | 308
|
|
21
|
-
| 400 | 401 | 403 | 404 | 405 | 409 | 410 | 413 | 415 | 422 | 429
|
|
22
|
-
| 500 | 501 | 502 | 503 | 504;
|
|
23
|
-
|
|
24
|
-
export type LambderResolverResponse = {
|
|
25
|
-
statusCode: HttpStatusCode,
|
|
26
|
-
multiValueHeaders?: Record<string, string[]>,
|
|
27
|
-
body: string | null,
|
|
28
|
-
isBase64Encoded?: boolean,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type LambderApiResponseConfig = {
|
|
32
|
-
versionExpired?: boolean;
|
|
33
|
-
sessionExpired?: boolean;
|
|
34
|
-
notAuthorized?: boolean;
|
|
35
|
-
message?: any;
|
|
36
|
-
errorMessage?: any;
|
|
37
|
-
logList?: any[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
42
|
-
payload?: T | null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export default class LambderResponseBuilder<TResponse = any> {
|
|
46
|
-
private isCorsEnabled: boolean;
|
|
47
|
-
private publicPath: string;
|
|
48
|
-
private apiVersion: string|null;
|
|
49
|
-
private lambderUtils: LambderUtils;
|
|
50
|
-
private ctx?: LambderRenderContext<any>;
|
|
51
|
-
|
|
52
|
-
constructor(
|
|
53
|
-
{ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx }:
|
|
54
|
-
{
|
|
55
|
-
isCorsEnabled: boolean,
|
|
56
|
-
publicPath: string,
|
|
57
|
-
apiVersion?: string|null,
|
|
58
|
-
lambderUtils: LambderUtils,
|
|
59
|
-
ctx?: LambderRenderContext<any>,
|
|
60
|
-
}
|
|
61
|
-
){
|
|
62
|
-
this.isCorsEnabled = isCorsEnabled;
|
|
63
|
-
this.publicPath = publicPath;
|
|
64
|
-
this.apiVersion = apiVersion ?? null;
|
|
65
|
-
this.lambderUtils = lambderUtils;
|
|
66
|
-
this.ctx = ctx;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
private async readPublicFileSync(filePath: string){
|
|
70
|
-
const fs = await getFS();
|
|
71
|
-
const path = await getPath();
|
|
72
|
-
|
|
73
|
-
if (!fs || !path) {
|
|
74
|
-
throw new Error("File system operations are not available in browser environment");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const publicPath = path.resolve(this.publicPath);
|
|
78
|
-
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
79
|
-
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
80
|
-
if(!absolutePath.startsWith(publicPath)){ return "forbidden-public-path"; }
|
|
81
|
-
return await fs.promises.readFile(absolutePath);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
private async checkPublicFileExist(filePath: string){
|
|
85
|
-
const fs = await getFS();
|
|
86
|
-
const path = await getPath();
|
|
87
|
-
|
|
88
|
-
if (!fs || !path) { return false; }
|
|
89
|
-
|
|
90
|
-
const publicPath = path.resolve(this.publicPath);
|
|
91
|
-
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
92
|
-
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
93
|
-
if(!absolutePath.startsWith(publicPath)){ return false; }
|
|
94
|
-
try {
|
|
95
|
-
const stat = await fs.promises.stat(absolutePath);
|
|
96
|
-
return stat.isFile();
|
|
97
|
-
} catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
addHeader(key: string, value: string){
|
|
103
|
-
if(!this.ctx) throw new Error(".addHeader function is not available within this hook");
|
|
104
|
-
else{
|
|
105
|
-
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key, value });
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
setHeader(key: string, value: string|string[]){
|
|
110
|
-
if(!this.ctx) throw new Error(".setHeader function is not available within this hook");
|
|
111
|
-
else{
|
|
112
|
-
this.ctx._otherInternal.addHeaderFnAccumulator = this.ctx._otherInternal.addHeaderFnAccumulator
|
|
113
|
-
.filter((header: { key: string, value: string }) => header.key !== key);
|
|
114
|
-
this.ctx._otherInternal.setHeaderFnAccumulator.push({ key, value });
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
logToApiResponse(input:any){
|
|
119
|
-
if(!this.ctx) throw new Error(".logToResponse function is not available within this hook");
|
|
120
|
-
else{
|
|
121
|
-
this.ctx._otherInternal.logToApiResponseAccumulator.push(input);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
raw(param: LambderResolverResponse){
|
|
127
|
-
return param;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
json(
|
|
131
|
-
data: Record<string, any>,
|
|
132
|
-
headers?: Record<string, string|string[]>
|
|
133
|
-
):LambderResolverResponse{
|
|
134
|
-
return this.raw({
|
|
135
|
-
statusCode: 200,
|
|
136
|
-
multiValueHeaders: {
|
|
137
|
-
"Content-Type": ["application/json; charset=utf-8"],
|
|
138
|
-
...(this.isCorsEnabled ? CORS_HEADERS : {}),
|
|
139
|
-
...convertToMultiHeader(headers)
|
|
140
|
-
},
|
|
141
|
-
body: JSON.stringify(data),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
xml(data: string):LambderResolverResponse{
|
|
146
|
-
return this.raw({
|
|
147
|
-
statusCode: 200,
|
|
148
|
-
isBase64Encoded: true,
|
|
149
|
-
multiValueHeaders: { "Content-Type": ["application/xml; charset=utf-8"]},
|
|
150
|
-
body: Buffer.from(data).toString("base64"),
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
html(
|
|
155
|
-
data: string,
|
|
156
|
-
headers?: Record<string, string|string[]>,
|
|
157
|
-
):LambderResolverResponse{
|
|
158
|
-
return this.raw({
|
|
159
|
-
statusCode: 200,
|
|
160
|
-
isBase64Encoded: true,
|
|
161
|
-
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
162
|
-
body: Buffer.from(data).toString("base64"),
|
|
163
|
-
});
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
redirect(
|
|
167
|
-
url: string,
|
|
168
|
-
statusCode: HttpStatusCode = 302,
|
|
169
|
-
headers?: Record<string, string|string[]>,
|
|
170
|
-
):LambderResolverResponse{
|
|
171
|
-
return this.raw({
|
|
172
|
-
statusCode: statusCode,
|
|
173
|
-
multiValueHeaders: { "Location" : [url], ...convertToMultiHeader(headers) },
|
|
174
|
-
body:null
|
|
175
|
-
});
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
status404(
|
|
179
|
-
data: string,
|
|
180
|
-
headers?: Record<string, string|string[]>,
|
|
181
|
-
):LambderResolverResponse{
|
|
182
|
-
return this.raw({
|
|
183
|
-
statusCode: 404,
|
|
184
|
-
isBase64Encoded: true,
|
|
185
|
-
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
186
|
-
body: Buffer.from(data).toString("base64"),
|
|
187
|
-
});
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
versionExpired(
|
|
191
|
-
headers?: Record<string, string|string[]>,
|
|
192
|
-
):LambderResolverResponse{
|
|
193
|
-
return this.api(null, { versionExpired: true }, headers);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
cors():LambderResolverResponse{
|
|
197
|
-
return this.raw({
|
|
198
|
-
statusCode: 200,
|
|
199
|
-
multiValueHeaders: this.isCorsEnabled ? CORS_HEADERS: {},
|
|
200
|
-
body: JSON.stringify(""),
|
|
201
|
-
});
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
fileBase64 (
|
|
205
|
-
fileBase64: string,
|
|
206
|
-
mimeType: string,
|
|
207
|
-
headers?: Record<string, string|string[]>,
|
|
208
|
-
):LambderResolverResponse{
|
|
209
|
-
return this.raw({
|
|
210
|
-
statusCode: 200,
|
|
211
|
-
isBase64Encoded: true,
|
|
212
|
-
multiValueHeaders: { "Content-Type": [mimeType || "application/octet-stream"], ...convertToMultiHeader(headers) },
|
|
213
|
-
body: fileBase64,
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
async file(
|
|
218
|
-
filePath: string,
|
|
219
|
-
headers?: Record<string, string|string[]>,
|
|
220
|
-
fallbackFilePath?: string,
|
|
221
|
-
):Promise<LambderResolverResponse>{
|
|
222
|
-
const doesFileExist = await this.checkPublicFileExist(filePath);
|
|
223
|
-
if(!doesFileExist){
|
|
224
|
-
if(fallbackFilePath){
|
|
225
|
-
const doesFallbackExist = await this.checkPublicFileExist(fallbackFilePath);
|
|
226
|
-
if(doesFallbackExist){
|
|
227
|
-
return await this.file(fallbackFilePath, headers);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return this.json({ error: "File not found: " + filePath })
|
|
231
|
-
}
|
|
232
|
-
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
233
|
-
const body = await this.readPublicFileSync(filePath);
|
|
234
|
-
if (body === "forbidden-public-path") {
|
|
235
|
-
throw new Error("Forbidden public path: " + filePath);
|
|
236
|
-
}
|
|
237
|
-
const bodyBuffer: Buffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
238
|
-
const bodyBase64 = bodyBuffer.toString("base64");
|
|
239
|
-
console.log("bodyBase64.length",bodyBase64.length);
|
|
240
|
-
return this.fileBase64(bodyBase64, mimeType || "application/octet-stream", headers);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
async ejsTemplate(
|
|
244
|
-
template: string,
|
|
245
|
-
pageData: Record<string, any>,
|
|
246
|
-
headers?: Record<string, string|string[]>,
|
|
247
|
-
):Promise<LambderResolverResponse>{
|
|
248
|
-
const renderedResult = await this.lambderUtils.renderEjs(template, pageData);
|
|
249
|
-
|
|
250
|
-
return this.raw({
|
|
251
|
-
statusCode: 200,
|
|
252
|
-
isBase64Encoded: true,
|
|
253
|
-
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
254
|
-
body: Buffer.from(renderedResult).toString("base64"),
|
|
255
|
-
});
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
async ejsFile(
|
|
259
|
-
filePath: string,
|
|
260
|
-
pageData: Record<string, any>,
|
|
261
|
-
headers?: Record<string, string|string[]>,
|
|
262
|
-
):Promise<LambderResolverResponse>{
|
|
263
|
-
const renderedResult = await this.lambderUtils.renderEjsFile(filePath, pageData);
|
|
264
|
-
|
|
265
|
-
return this.raw({
|
|
266
|
-
statusCode: 200,
|
|
267
|
-
isBase64Encoded: true,
|
|
268
|
-
multiValueHeaders: {"Content-Type": ["text/html; charset=utf-8"], ...convertToMultiHeader(headers)},
|
|
269
|
-
body: Buffer.from(renderedResult).toString("base64"),
|
|
270
|
-
});
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
api(
|
|
274
|
-
payload: TResponse | null,
|
|
275
|
-
{
|
|
276
|
-
versionExpired, sessionExpired, notAuthorized,
|
|
277
|
-
message, errorMessage, logList,
|
|
278
|
-
}: LambderApiResponseConfig = {
|
|
279
|
-
versionExpired: undefined, sessionExpired: undefined, notAuthorized: undefined,
|
|
280
|
-
message: null, errorMessage: null, logList: undefined
|
|
281
|
-
},
|
|
282
|
-
headers?: Record<string, string|string[]>,
|
|
283
|
-
): LambderResolverResponse {
|
|
284
|
-
const finalLogList = logList || this.ctx?._otherInternal?.logToApiResponseAccumulator;
|
|
285
|
-
return this.json({
|
|
286
|
-
apiVersion: this.apiVersion,
|
|
287
|
-
payload,
|
|
288
|
-
...(versionExpired ? {versionExpired} : {}),
|
|
289
|
-
...(sessionExpired ? {sessionExpired} : {}),
|
|
290
|
-
...(notAuthorized ? {notAuthorized} : {}),
|
|
291
|
-
...(message ? {message} : {}),
|
|
292
|
-
...(errorMessage ? {errorMessage} : {}),
|
|
293
|
-
...(finalLogList?.length ? {logList: finalLogList} : {}),
|
|
294
|
-
}, headers);
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
apiBinary<T=any>(
|
|
298
|
-
payload: T | null,
|
|
299
|
-
{
|
|
300
|
-
versionExpired, sessionExpired, notAuthorized,
|
|
301
|
-
message, errorMessage, logList,
|
|
302
|
-
}: LambderApiResponseConfig = {
|
|
303
|
-
versionExpired: undefined, sessionExpired: undefined, notAuthorized: undefined,
|
|
304
|
-
message: null, errorMessage: null, logList: undefined
|
|
305
|
-
},
|
|
306
|
-
headers?: Record<string, string|string[]>,
|
|
307
|
-
): LambderResolverResponse {
|
|
308
|
-
const finalLogList = logList || this.ctx?._otherInternal?.logToApiResponseAccumulator;
|
|
309
|
-
const result = {
|
|
310
|
-
apiVersion: this.apiVersion,
|
|
311
|
-
payload,
|
|
312
|
-
...(versionExpired ? {versionExpired} : {}),
|
|
313
|
-
...(sessionExpired ? {sessionExpired} : {}),
|
|
314
|
-
...(notAuthorized ? {notAuthorized} : {}),
|
|
315
|
-
...(message ? {message} : {}),
|
|
316
|
-
...(errorMessage ? {errorMessage} : {}),
|
|
317
|
-
...(finalLogList?.length ? {logList: finalLogList} : {}),
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
return this.raw({
|
|
321
|
-
statusCode: 200,
|
|
322
|
-
isBase64Encoded: true,
|
|
323
|
-
multiValueHeaders: {
|
|
324
|
-
"Content-Type": ["application/lambder-json-stream"],
|
|
325
|
-
"Content-Encoding": ["gzip"],
|
|
326
|
-
...convertToMultiHeader(headers)
|
|
327
|
-
},
|
|
328
|
-
body: Buffer.from(JSON.stringify(result)).toString("base64"),
|
|
329
|
-
});
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
};
|