lambder 1.0.147 → 2.0.2
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 +355 -410
- package/dist/Lambder.d.ts +47 -21
- package/dist/Lambder.js +79 -24
- package/dist/LambderApiContract.d.ts +10 -42
- package/dist/LambderApiContract.js +2 -22
- package/dist/LambderCaller.js +2 -2
- package/dist/LambderMSW.js +0 -4
- package/dist/LambderResolver.d.ts +16 -17
- package/dist/LambderResponseBuilder.d.ts +2 -3
- package/dist/LambderResponseBuilder.js +1 -3
- package/dist/LambderUtils.js +1 -3
- package/dist/index.d.ts +1 -1
- package/docs/LAMBDER_MSW.md +6 -6
- package/docs/TYPE_SAFE_QUICK_START.md +54 -177
- package/examples/msw-testing-example.ts +36 -33
- package/examples/secure-session-example.ts +50 -34
- package/examples/zod-chained-api-example.ts +63 -0
- package/package.json +3 -2
- package/src/Lambder.ts +124 -83
- package/src/LambderApiContract.ts +7 -50
- package/src/LambderCaller.ts +2 -2
- package/src/LambderMSW.ts +0 -7
- package/src/LambderResolver.ts +21 -24
- package/src/LambderResponseBuilder.ts +4 -7
- package/src/LambderUtils.ts +1 -3
- package/src/index.ts +0 -3
- package/tests/UNTESTED_FEATURES.md +263 -0
- package/tests/error-handling.test.ts +585 -0
- package/tests/hooks.test.ts +561 -0
- package/tests/output-type-runtime.test.ts +80 -64
- package/tests/routes.test.ts +542 -0
- package/tests/session.test.ts +38 -24
- package/tests/type-safety.test.ts +147 -97
- package/tests/use-plugin.test.ts +437 -0
- package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
- package/examples/output-type-enforcement-example.ts +0 -218
- package/examples/simplified-typed-api-example.ts +0 -365
- package/examples/test-output-type-enforcement.ts +0 -101
- package/test-type-enforcement.ts +0 -111
package/Readme.md
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
# Lambder - Serverless NodeJS Web Framework
|
|
1
|
+
# Lambder - Serverless NodeJS Web Framework (v2.0)
|
|
2
2
|
|
|
3
3
|
Lambder is a highly opinionated dynamic serverless framework designed to facilitate the management and implementation of routes and APIs within AWS Lambda functions, specifically tailored for TypeScript projects. It provides a streamlined approach to handling HTTP requests, managing sessions, and defining API routes, making serverless application development more intuitive and structured.
|
|
4
4
|
|
|
5
|
+
**New in v2.0:** Full type safety with Zod schemas and runtime validation!
|
|
6
|
+
|
|
5
7
|
## Features
|
|
6
8
|
|
|
9
|
+
- **Type-Safe APIs with Zod**: Define inputs and outputs with Zod schemas. Get automatic runtime validation and compile-time type inference.
|
|
10
|
+
- **Method Chaining**: Build your API contract incrementally with a fluent interface.
|
|
7
11
|
- **Simple API & Route Declaration**: Define your APIs and routes using concise and expressive syntax.
|
|
8
12
|
- **Session Management**: Built-in session management to secure and personalize user experiences.
|
|
9
|
-
- **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle
|
|
13
|
+
- **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle.
|
|
10
14
|
- **Error Handling**: Comprehensive error handling capabilities, including global error handlers and route-specific fallbacks.
|
|
11
|
-
- **Seamless Integration**: Designed to work effortlessly with AWS Lambda and API Gateway
|
|
15
|
+
- **Seamless Integration**: Designed to work effortlessly with AWS Lambda and API Gateway.
|
|
12
16
|
|
|
13
17
|
## Installation
|
|
14
18
|
|
|
15
|
-
To include Lambder in your TypeScript project, you can install it using npm or yarn. First, ensure that you have TypeScript set up in your project.
|
|
16
|
-
|
|
17
19
|
```bash
|
|
18
|
-
npm install lambder
|
|
20
|
+
npm install lambder zod
|
|
19
21
|
# or
|
|
20
|
-
yarn add lambder
|
|
22
|
+
yarn add lambder zod
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
## Backend Usage
|
|
@@ -25,296 +27,300 @@ yarn add lambder
|
|
|
25
27
|
### Basic Setup
|
|
26
28
|
|
|
27
29
|
```typescript
|
|
28
|
-
import Lambder from 'lambder';
|
|
30
|
+
import Lambder, { InferLambderContract } from 'lambder';
|
|
31
|
+
import { z } from 'zod';
|
|
29
32
|
import * as path from 'path';
|
|
30
33
|
|
|
31
34
|
const lambder = new Lambder({
|
|
32
|
-
apiPath: "/
|
|
35
|
+
apiPath: "/api",
|
|
33
36
|
publicPath: path.resolve(`./public`),
|
|
34
|
-
// ejsPath: path.resolve(`./ejs-templates`),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Enable session
|
|
39
|
-
lambder.enableDdbSession({
|
|
40
|
-
tableName: "website-session", // DynamoDB Table Name
|
|
41
|
-
tableRegion: "us-east-1", // DynamoDB Table Region
|
|
42
|
-
sessionSalt: "8p6Vt+4b1w3N8d/dcJ47QF3DRkp9koFg0G" // Change salt
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Enable Cors
|
|
46
|
-
lambder.enableCors(true);
|
|
47
|
-
|
|
48
|
-
// Define a simple api
|
|
49
|
-
lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
|
|
50
|
-
const companyName = apiPayload.companyName;
|
|
51
|
-
const data = await fetchDataSomehow(companyName);
|
|
52
|
-
return res.api(data);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Start a session from an API
|
|
56
|
-
lambder.addApi("loginUser", async (ctx, res) => {
|
|
57
|
-
const user = await fetchUserData();
|
|
58
|
-
await lambder.getSessionController(ctx).createSession(user.id);
|
|
59
|
-
return res.api({ success: true });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Define a simple route
|
|
63
|
-
lambder.addRoute("/hello-world", (ctx, res) => {
|
|
64
|
-
return res.html("Hello World");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Route with parameters
|
|
68
|
-
lambder.addRoute("/user/:userId", async (ctx, res) => {
|
|
69
|
-
const user = await getUser(ctx.pathParams.userId);
|
|
70
|
-
if(!user) return res.status404("Not found");
|
|
71
|
-
|
|
72
|
-
return res.html(`Hello ${user.name}`);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Define a regex route
|
|
76
|
-
lambder.addRoute(/\/hello-regex/, (ctx, res) => {
|
|
77
|
-
return res.html("Hello Regex");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Function routes allows routing on any context variable.
|
|
81
|
-
lambder.addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
|
|
82
|
-
return res.html("Hello from a function route");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Define a simple route that serves an EJS template file
|
|
86
|
-
lambder.addRoute("/product/:productId", (ctx, res) => {
|
|
87
|
-
const product = await getProduct(ctx.pathParams.productId);
|
|
88
|
-
// Serve the file from ejsPath defined above.
|
|
89
|
-
return await res.ejsFile("productPage.html.ejs", { product });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Serve sitemap using an ejs template.
|
|
93
|
-
lambder.addRoute("/sitemap", (ctx, res) => {
|
|
94
|
-
const templateString = `
|
|
95
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
96
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
97
|
-
<%~
|
|
98
|
-
page.urlList.map(url => `<url><loc>${url}</loc></url>`).join("")
|
|
99
|
-
%>
|
|
100
|
-
</urlset>
|
|
101
|
-
`.trim();
|
|
102
|
-
const urlList = [];
|
|
103
|
-
return await res.ejsTemplate(templateString, { urlList }, { "Content-Type": ["application/xml; charset=utf-8"]});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Match all other paths and serve static files from publicPath, if not found, serve index.html
|
|
108
|
-
lambder.addRoute("/(.*)", (ctx, res)=>{
|
|
109
|
-
return res.file(ctx.path, {}, "index.html");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Set a fallback handler for unmatched routes
|
|
114
|
-
lambder.setRouteFallbackHandler((ctx, res) => {
|
|
115
|
-
return res.status404("Not Found");
|
|
37
|
+
// ejsPath: path.resolve(`./ejs-templates`), // Optional
|
|
116
38
|
});
|
|
117
39
|
|
|
118
|
-
//
|
|
119
|
-
lambder
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
40
|
+
// Enable session and CORS - all chainable!
|
|
41
|
+
lambder
|
|
42
|
+
.enableDdbSession({
|
|
43
|
+
tableName: "website-session",
|
|
44
|
+
tableRegion: "us-east-1",
|
|
45
|
+
sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING"
|
|
46
|
+
})
|
|
47
|
+
.enableCors(true);
|
|
48
|
+
|
|
49
|
+
// Define type-safe APIs with Zod schemas
|
|
50
|
+
const app = lambder
|
|
51
|
+
.addApi("getCompanyPage", {
|
|
52
|
+
input: z.object({ companyName: z.string() }),
|
|
53
|
+
output: z.object({ id: z.string(), name: z.string(), description: z.string() })
|
|
54
|
+
}, async ({ apiPayload }, res) => {
|
|
55
|
+
// apiPayload is automatically typed and validated!
|
|
56
|
+
const data = await fetchDataSomehow(apiPayload.companyName);
|
|
57
|
+
return res.api(data); // Return value is type-checked
|
|
58
|
+
})
|
|
59
|
+
.addApi("loginUser", {
|
|
60
|
+
input: z.object({ email: z.string().email(), password: z.string() }),
|
|
61
|
+
output: z.object({ success: z.boolean(), token: z.string().optional() })
|
|
62
|
+
}, async (ctx, res) => {
|
|
63
|
+
const user = await authenticateUser(ctx.apiPayload.email, ctx.apiPayload.password);
|
|
64
|
+
if (!user) {
|
|
65
|
+
return res.api({ success: false });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await lambder.getSessionController(ctx).createSession(user.id);
|
|
69
|
+
return res.api({ success: true, token: "session-token" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Export the inferred contract for the frontend
|
|
73
|
+
export type AppContract = typeof lambder.ApiContractType;
|
|
74
|
+
|
|
75
|
+
// Export the handler
|
|
76
|
+
export const handler = lambder.getHandler();
|
|
127
77
|
```
|
|
128
78
|
|
|
79
|
+
### Adding Routes
|
|
129
80
|
|
|
81
|
+
Routes are fully chainable for a fluent interface.
|
|
130
82
|
|
|
131
|
-
### Adding APIs
|
|
132
|
-
|
|
133
|
-
For more details on route matching, please check [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package.
|
|
134
83
|
```typescript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
84
|
+
lambder
|
|
85
|
+
// Define a simple route
|
|
86
|
+
.addRoute("/hello-world", (ctx, res) => {
|
|
87
|
+
return res.html("Hello World");
|
|
88
|
+
})
|
|
89
|
+
// Route with parameters
|
|
90
|
+
.addRoute("/user/:userId", async (ctx, res) => {
|
|
91
|
+
const user = await getUser(ctx.pathParams.userId);
|
|
92
|
+
if(!user) return res.status404("Not found");
|
|
93
|
+
return res.html(`Hello ${user.name}`);
|
|
94
|
+
})
|
|
95
|
+
// Define a regex route
|
|
96
|
+
.addRoute(/\/hello-regex/, (ctx, res) => {
|
|
97
|
+
return res.html("Hello Regex");
|
|
98
|
+
})
|
|
99
|
+
// Function routes allows routing on any context variable
|
|
100
|
+
.addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
|
|
101
|
+
return res.html("Hello from a function route");
|
|
102
|
+
})
|
|
103
|
+
// Define a simple route that serves an EJS template file
|
|
104
|
+
.addRoute("/product/:productId", async (ctx, res) => {
|
|
105
|
+
const product = await getProduct(ctx.pathParams.productId);
|
|
106
|
+
// Serve the file from ejsPath defined above.
|
|
107
|
+
return await res.ejsFile("productPage.html.ejs", { product });
|
|
108
|
+
})
|
|
109
|
+
// Serve sitemap using an ejs template
|
|
110
|
+
.addRoute("/sitemap", async (ctx, res) => {
|
|
111
|
+
const templateString = `
|
|
112
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
114
|
+
<%~
|
|
115
|
+
page.urlList.map(url => \`<url><loc>\${url}</loc></url>\`).join("")
|
|
116
|
+
%>
|
|
117
|
+
</urlset>
|
|
118
|
+
`.trim();
|
|
119
|
+
const urlList = await getUrlList();
|
|
120
|
+
return await res.ejsTemplate(templateString, { urlList }, { "Content-Type": ["application/xml; charset=utf-8"]});
|
|
121
|
+
})
|
|
122
|
+
// Match all other paths and serve static files from publicPath
|
|
123
|
+
.addRoute("/(.*)", (ctx, res)=>{
|
|
124
|
+
return res.file(ctx.path, {}, "index.html");
|
|
125
|
+
})
|
|
126
|
+
// Set a fallback handler for unmatched routes
|
|
127
|
+
.setRouteFallbackHandler((ctx, res) => {
|
|
128
|
+
return res.status404("Not Found");
|
|
129
|
+
})
|
|
130
|
+
// Global error handler
|
|
131
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
132
|
+
console.error("Error:", err);
|
|
133
|
+
return res.raw({ statusCode: 500, body: "Internal Server Error" });
|
|
134
|
+
});
|
|
155
135
|
```
|
|
156
136
|
|
|
137
|
+
### Adding APIs
|
|
157
138
|
|
|
158
|
-
|
|
159
|
-
// Define a simple api
|
|
160
|
-
lambder.addApi("getCompanyPage", async (ctx, res) => {
|
|
161
|
-
const {
|
|
162
|
-
host, path, get, post, cookie, headers,
|
|
163
|
-
apiName, apiPayload
|
|
164
|
-
} = ctx;
|
|
165
|
-
const companyName = apiPayload.companyName;
|
|
166
|
-
const data = await fetchDataSomehow(companyName);
|
|
167
|
-
return res.api(data);
|
|
168
|
-
});
|
|
169
|
-
```
|
|
139
|
+
All APIs in v2.0 must use Zod schemas for type safety and runtime validation. Use method chaining for a clean API definition.
|
|
170
140
|
|
|
171
141
|
```typescript
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
142
|
+
import { z } from 'zod';
|
|
143
|
+
|
|
144
|
+
lambder
|
|
145
|
+
// Add a typed API
|
|
146
|
+
.addApi("getUserById", {
|
|
147
|
+
input: z.object({ userId: z.string() }),
|
|
148
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
149
|
+
}, async (ctx, res) => {
|
|
150
|
+
// ctx.apiPayload is typed as { userId: string }
|
|
151
|
+
const user = await db.getUser(ctx.apiPayload.userId);
|
|
152
|
+
return res.api(user); // Type-checked against output schema
|
|
153
|
+
})
|
|
154
|
+
// Session-protected API
|
|
155
|
+
.addSessionApi("getProfile", {
|
|
156
|
+
input: z.void(),
|
|
157
|
+
output: z.object({ userId: z.string(), username: z.string() })
|
|
158
|
+
}, async (ctx, res) => {
|
|
159
|
+
// Session is automatically fetched and validated
|
|
160
|
+
// ctx.session.data contains your session data
|
|
161
|
+
return res.api({
|
|
162
|
+
userId: ctx.session.data.userId,
|
|
163
|
+
username: ctx.session.data.username
|
|
164
|
+
});
|
|
165
|
+
});
|
|
183
166
|
```
|
|
184
167
|
|
|
168
|
+
### Modular APIs with .use()
|
|
185
169
|
|
|
170
|
+
For larger applications, split your APIs into separate modules:
|
|
186
171
|
|
|
187
|
-
### Adding Routes
|
|
188
172
|
```typescript
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
|
|
173
|
+
// user-api.ts
|
|
174
|
+
import { z } from "zod";
|
|
175
|
+
import Lambder from "lambder";
|
|
176
|
+
|
|
177
|
+
export const userApi = <T>(l: Lambder<T>) => {
|
|
178
|
+
return l
|
|
179
|
+
.addApi("getUser", {
|
|
180
|
+
input: z.object({ id: z.string() }),
|
|
181
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
182
|
+
}, async (ctx, res) => {
|
|
183
|
+
return res.api({ id: ctx.apiPayload.id, name: "User" });
|
|
184
|
+
})
|
|
185
|
+
.addApi("createUser", {
|
|
186
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
187
|
+
output: z.object({ id: z.string() })
|
|
188
|
+
}, async (ctx, res) => {
|
|
189
|
+
return res.api({ id: "123" });
|
|
190
|
+
});
|
|
191
|
+
};
|
|
197
192
|
|
|
193
|
+
// index.ts
|
|
194
|
+
import { userApi } from "./user-api";
|
|
198
195
|
|
|
199
|
-
|
|
196
|
+
const lambder = new Lambder()
|
|
197
|
+
.use(userApi);
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
// Before render hook example
|
|
203
|
-
lambder.addHook("beforeRender", async (ctx, res) => {
|
|
204
|
-
// Perform actions before rendering
|
|
205
|
-
return ctx; // Return modified context or throw an Error
|
|
206
|
-
});
|
|
199
|
+
export type AppContract = typeof lambder.ApiContractType;
|
|
207
200
|
```
|
|
208
201
|
|
|
209
202
|
|
|
210
|
-
###
|
|
203
|
+
### Hooks
|
|
204
|
+
|
|
205
|
+
Lambder provides hooks to execute code at different stages of the request lifecycle.
|
|
211
206
|
|
|
212
207
|
```typescript
|
|
213
|
-
lambder
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
208
|
+
lambder
|
|
209
|
+
// Before render hook
|
|
210
|
+
.addHook("beforeRender", async (ctx, res) => {
|
|
211
|
+
// Perform actions before rendering
|
|
212
|
+
console.log("Request received:", ctx.path);
|
|
213
|
+
return ctx; // Return modified context or throw an Error
|
|
214
|
+
})
|
|
215
|
+
// After render hook
|
|
216
|
+
.addHook("afterRender", async (ctx, res, response) => {
|
|
217
|
+
// Modify response before sending
|
|
218
|
+
console.log("Response status:", response.statusCode);
|
|
219
|
+
return response;
|
|
220
|
+
});
|
|
217
221
|
```
|
|
218
222
|
|
|
219
223
|
### Session Management
|
|
220
224
|
|
|
221
|
-
|
|
225
|
+
Enable DynamoDB-based session management - fully chainable:
|
|
222
226
|
|
|
223
227
|
```typescript
|
|
224
|
-
// Enable sessions using a
|
|
225
|
-
lambder
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
// Enable sessions using a DynamoDB session table - chainable!
|
|
229
|
+
const lambder = new Lambder({ apiPath: '/api', publicPath: './public' })
|
|
230
|
+
.enableDdbSession({
|
|
231
|
+
tableName: "website-session",
|
|
232
|
+
tableRegion: "us-east-1",
|
|
233
|
+
sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
|
|
234
|
+
enableSlidingExpiration: true // Optional: extend session on each access
|
|
235
|
+
})
|
|
236
|
+
.enableCors(true)
|
|
237
|
+
.addApi(...);
|
|
230
238
|
```
|
|
231
|
-
#### DynamoDB Session Table Structure:
|
|
232
239
|
|
|
233
|
-
|
|
240
|
+
#### DynamoDB Session Table Structure
|
|
234
241
|
|
|
235
242
|
- Primary Key: "pk"
|
|
236
243
|
- Sort Key: "sk"
|
|
237
|
-
- TTL Key: "expiresAt" (optional)
|
|
244
|
+
- TTL Key: "expiresAt" (optional, recommended)
|
|
245
|
+
|
|
246
|
+
See [docs/DYNAMODB_SETUP.md](docs/DYNAMODB_SETUP.md) for detailed setup instructions.
|
|
238
247
|
|
|
239
248
|
#### Session Controller
|
|
240
249
|
|
|
241
|
-
After
|
|
250
|
+
After enabling sessions, you can access the session controller:
|
|
242
251
|
|
|
243
252
|
```typescript
|
|
244
|
-
|
|
245
|
-
const sessionController = lambder.getSessionController(ctx);
|
|
253
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
246
254
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Starts a new session and persists the session data to DDB.
|
|
255
|
+
// Available methods:
|
|
256
|
+
await sessionController.createSession(sessionKey, data, ttlInSeconds);
|
|
257
|
+
// Starts a new session and persists the session data to DDB.
|
|
251
258
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
await sessionController.fetchSession();
|
|
260
|
+
// Fetch and validate if there is an existing session
|
|
261
|
+
// This is automatically done for addSessionRoute and addSessionApi
|
|
262
|
+
// Throws if session not found
|
|
256
263
|
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
await sessionController.fetchSessionIfExists();
|
|
265
|
+
// Returns session if found, otherwise null
|
|
259
266
|
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
await sessionController.updateSessionData(updatedData);
|
|
268
|
+
// Updates the active session's data and persists it to DDB
|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
async endSessionAll():
|
|
267
|
-
// Ends and deletes all registered sessions for this sessionKey across all devices
|
|
268
|
-
}
|
|
269
|
-
*/
|
|
270
|
-
```
|
|
270
|
+
await sessionController.endSession();
|
|
271
|
+
// End session and delete from DDB
|
|
271
272
|
|
|
273
|
+
await sessionController.endSessionAll();
|
|
274
|
+
// Ends and deletes all sessions for this sessionKey across all devices
|
|
272
275
|
|
|
276
|
+
await sessionController.regenerateSession();
|
|
277
|
+
// Regenerates session token (use after password change, etc.)
|
|
278
|
+
```
|
|
273
279
|
|
|
274
280
|
#### Session Examples
|
|
275
|
-
```typescript
|
|
276
|
-
lambder.addApi("getCompanyPage", async (ctx, res) => {
|
|
277
|
-
|
|
278
|
-
// createSession: Start a new session
|
|
279
|
-
const userId = "37234";
|
|
280
|
-
await lambder.getSessionController(ctx)
|
|
281
|
-
.createSession(userId, { "business": "Session data goes here" });
|
|
282
|
-
console.log(ctx.session?.sessionKey); // "37234"
|
|
283
|
-
console.log(ctx.session?.data?.business); // "Session data goes here"
|
|
284
|
-
|
|
285
|
-
// fetchSession: Fetch and validate if there is an existing session
|
|
286
|
-
// This is automatically done for addSessionRoute and addSessionApi
|
|
287
|
-
await lambder.getSessionController(ctx).fetchSession();
|
|
288
|
-
console.log(ctx.session?.sessionKey); // "37234"
|
|
289
|
-
|
|
290
|
-
// updateSessionData: Updates the active sessions data and persist it to ddb.
|
|
291
|
-
await lambder.getSessionController(ctx)
|
|
292
|
-
.updateSessionData({ "business2": "Session data updated" });
|
|
293
|
-
console.log(ctx.session?.sessionKey); // "37234"
|
|
294
|
-
console.log(ctx.session?.data?.business); // undefined
|
|
295
|
-
console.log(ctx.session?.data?.business2); // "Session data updated"
|
|
296
|
-
|
|
297
|
-
// endSession: Ends the session and removes it from ddb
|
|
298
|
-
await lambder.getSessionController(ctx).endSession(); // End session
|
|
299
|
-
console.log(ctx.session?.sessionKey); // undefined
|
|
300
|
-
console.log(ctx.session?.data?.business); // undefined
|
|
301
|
-
|
|
302
|
-
// endSessionAll: Ends all registered sessions for this user in all devices.
|
|
303
|
-
await lambder.getSessionController(ctx).endSessionAll();
|
|
304
|
-
console.log(ctx.session?.sessionKey); // undefined
|
|
305
|
-
console.log(ctx.session?.data?.business); // undefined
|
|
306
281
|
|
|
307
|
-
|
|
282
|
+
```typescript
|
|
283
|
+
lambder
|
|
284
|
+
.addApi("createSession", {
|
|
285
|
+
input: z.object({ userId: z.string() }),
|
|
286
|
+
output: z.object({ success: z.boolean() })
|
|
287
|
+
}, async (ctx, res) => {
|
|
288
|
+
// Create a new session
|
|
289
|
+
const userId = ctx.apiPayload.userId;
|
|
290
|
+
await lambder.getSessionController(ctx)
|
|
291
|
+
.createSession(userId, { business: "Session data goes here" });
|
|
292
|
+
|
|
293
|
+
console.log(ctx.session?.sessionKey); // userId
|
|
294
|
+
console.log(ctx.session?.data?.business); // "Session data goes here"
|
|
295
|
+
|
|
296
|
+
return res.api({ success: true });
|
|
297
|
+
})
|
|
298
|
+
.addSessionApi("updateSession", {
|
|
299
|
+
input: z.object({ newData: z.string() }),
|
|
300
|
+
output: z.object({ success: z.boolean() })
|
|
301
|
+
}, async (ctx, res) => {
|
|
302
|
+
// Session is automatically fetched
|
|
303
|
+
console.log(ctx.session.sessionKey); // userId
|
|
304
|
+
|
|
305
|
+
// Update session data
|
|
306
|
+
await lambder.getSessionController(ctx)
|
|
307
|
+
.updateSessionData({ business2: ctx.apiPayload.newData });
|
|
308
|
+
|
|
309
|
+
console.log(ctx.session.data.business); // undefined
|
|
310
|
+
console.log(ctx.session.data.business2); // newData value
|
|
311
|
+
|
|
312
|
+
return res.api({ success: true });
|
|
313
|
+
});
|
|
308
314
|
```
|
|
309
315
|
|
|
310
|
-
### EJS Templates
|
|
316
|
+
### EJS Templates
|
|
311
317
|
|
|
312
318
|
EJS templates have the variables `page` and `partial` available:
|
|
313
319
|
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
- **Template**: The main file called with `await res.ejsFile('template-file')`. Has `page` variable.
|
|
321
|
+
- **Partial**: Included from a template with `<%- await include('partial/header.html.ejs', partialData) -%>`. Has both `page` and `partial` variables.
|
|
316
322
|
|
|
317
|
-
|
|
323
|
+
Example template:
|
|
318
324
|
```html
|
|
319
325
|
<div>
|
|
320
326
|
<%- await include('partial/header.html.ejs', partialData) -%>
|
|
@@ -322,7 +328,8 @@ An example template:
|
|
|
322
328
|
<%- await include('partial/footer.html.ejs', partialData) -%>
|
|
323
329
|
</div>
|
|
324
330
|
```
|
|
325
|
-
|
|
331
|
+
|
|
332
|
+
Example partial:
|
|
326
333
|
```html
|
|
327
334
|
<div>
|
|
328
335
|
<div>Page Variable: <pre><%~ JSON.stringify(page, null, 2) %></pre></div>
|
|
@@ -330,236 +337,170 @@ An example partial:
|
|
|
330
337
|
</div>
|
|
331
338
|
```
|
|
332
339
|
|
|
333
|
-
###
|
|
340
|
+
### Render Context (ctx) Variables
|
|
334
341
|
|
|
335
|
-
Add your imports to index.ts:
|
|
336
342
|
```typescript
|
|
337
|
-
lambder
|
|
343
|
+
lambder
|
|
344
|
+
.addApi("exampleApi", {
|
|
345
|
+
input: z.object({ value: z.string() }),
|
|
346
|
+
output: z.object({ result: z.string() })
|
|
347
|
+
}, async (ctx, res) => {
|
|
348
|
+
const {
|
|
349
|
+
host, // Request host: "www.example.com"
|
|
350
|
+
path, // Request path: "/api"
|
|
351
|
+
get, // GET query parameters: { userId: "342" }
|
|
352
|
+
post, // POST body (parsed): { userId: "342" }
|
|
353
|
+
cookie, // Cookies: { "rememberMe": "true" }
|
|
354
|
+
headers, // Request headers
|
|
355
|
+
apiName, // API name: "exampleApi"
|
|
356
|
+
apiPayload, // Validated input (same as post.payload)
|
|
357
|
+
session, // Session (null for addApi, available for addSessionApi)
|
|
358
|
+
} = ctx;
|
|
359
|
+
|
|
360
|
+
return res.api({ result: ctx.apiPayload.value });
|
|
361
|
+
});
|
|
338
362
|
```
|
|
339
363
|
|
|
340
|
-
|
|
364
|
+
### Resolver Methods
|
|
365
|
+
|
|
366
|
+
Available response methods:
|
|
367
|
+
|
|
341
368
|
```typescript
|
|
342
|
-
|
|
369
|
+
return res.raw(param);
|
|
370
|
+
// Sends a custom HTTP response. Useful for non-standard responses.
|
|
343
371
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// lambder.addApi(...)
|
|
347
|
-
};
|
|
348
|
-
```
|
|
372
|
+
return res.json(data, headers);
|
|
373
|
+
// Sends a JSON response with optional headers.
|
|
349
374
|
|
|
350
|
-
|
|
375
|
+
return res.xml(data);
|
|
376
|
+
// Sends an XML response (base64 encoded).
|
|
351
377
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
lambder.addApi("getCompanyName", async (ctx, res) => {
|
|
355
|
-
const {
|
|
356
|
-
host, // Request host. Exp: "www.example.com"
|
|
357
|
-
path, // Request path. Exp: "/index.html" or "/user/342"
|
|
358
|
-
get, // Get request query in JSON format. Exp: { userId: 342 }
|
|
359
|
-
post, // Post request body after JSON parsed. Exp: { userId: 342 }
|
|
360
|
-
cookie, // Cookies in an object. Exp: { "rememberMe": "true" }
|
|
361
|
-
headers, // Request Headers in an object. Exp: { "User-Agent": "....", ... }
|
|
362
|
-
apiName, // In this function it would return "getCompanyName"
|
|
363
|
-
apiPayload, // Same as post.payload
|
|
364
|
-
session, // Stores session. Only available in addSessionRoute and addSessionApi, otherwise null.
|
|
365
|
-
} = ctx;
|
|
366
|
-
return res.json({});
|
|
367
|
-
});
|
|
368
|
-
```
|
|
378
|
+
return res.html(data, headers);
|
|
379
|
+
// Sends an HTML response (base64 encoded).
|
|
369
380
|
|
|
370
|
-
|
|
381
|
+
return res.status301(url, headers);
|
|
382
|
+
// Redirects to the specified URL with a 301 status code.
|
|
371
383
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return res.raw(param);
|
|
375
|
-
// Sends a custom HTTP response defined by the param object.
|
|
376
|
-
// Useful for sending non-standard responses.
|
|
377
|
-
return res.json(data, headers);
|
|
378
|
-
// Sends a JSON response with the specified data and optional headers.
|
|
379
|
-
// It sets the Content-Type header to application/json.
|
|
380
|
-
|
|
381
|
-
return res.xml(data);
|
|
382
|
-
// Sends an XML response with the given data.
|
|
383
|
-
// Automatically encodes the response in base64 and sets
|
|
384
|
-
// the Content-Type header to application/xml.
|
|
385
|
-
|
|
386
|
-
return res.html(data, headers);
|
|
387
|
-
// Sends an HTML response containing the provided data with optional headers.
|
|
388
|
-
// The response is base64 encoded, and the Content-Type header is set to text/html.
|
|
389
|
-
|
|
390
|
-
return res.status301(url, headers);
|
|
391
|
-
// Redirects the client to the specified url with a 301 status code and optional headers.
|
|
392
|
-
// Useful for permanent redirections.
|
|
393
|
-
|
|
394
|
-
return res.status404(data, headers);
|
|
395
|
-
// Sends a 404 Not Found response with custom data and optional headers.
|
|
396
|
-
// The response is base64 encoded, and the Content-Type header is set to text/html.
|
|
397
|
-
|
|
398
|
-
return res.cors();
|
|
399
|
-
// Sends a 200 OK response with CORS headers enabled.
|
|
400
|
-
// This is typically used in response to a preflight request in a CORS scenario.
|
|
401
|
-
|
|
402
|
-
return res.fileBase64(fileBase64, mimeType, headers);
|
|
403
|
-
// Sends a file response with the content provided in base64 format,
|
|
404
|
-
// the specified mimeType, and optional headers.
|
|
405
|
-
|
|
406
|
-
return res.file(filePath, headers, fallbackFilePath);
|
|
407
|
-
// Serves a file from the server's public directory, with optional headers.
|
|
408
|
-
// If the file is not found and a fallbackFilePath is provided,
|
|
409
|
-
// attempts to serve the fallback file.
|
|
410
|
-
// Returns a JSON error if neither file is found.
|
|
411
|
-
|
|
412
|
-
return res.ejsFile(filePath, pageData, headers);
|
|
413
|
-
// Renders and serves an ejs file from the server's ejs directory, with optional headers.
|
|
414
|
-
// Returns a JSON error if file is not found.
|
|
415
|
-
|
|
416
|
-
return res.ejsTemplate(template, pageData, headers);
|
|
417
|
-
// Renders and serves an ejs template string, with optional headers.
|
|
418
|
-
|
|
419
|
-
return res.api(payload, { notAuthorized, message, errorMessage }, headers);
|
|
420
|
-
// This function works together with the LambderCaller from the frontend.
|
|
421
|
-
// Sends a standardized API response including the payload and status
|
|
422
|
-
// flags like versionExpired, sessionExpired, notAuthorized, along with
|
|
423
|
-
// optional messages and headers.
|
|
424
|
-
|
|
425
|
-
// res.die.*
|
|
426
|
-
// Acts the same as res.* but will:
|
|
427
|
-
// - Immediately return the value.
|
|
428
|
-
// - Skip the afterRender hooks.
|
|
429
|
-
|
|
430
|
-
return res.die.raw(param);
|
|
431
|
-
return res.die.json(data, headers);
|
|
432
|
-
return res.die.xml(data);
|
|
433
|
-
return res.die.html(data, headers);
|
|
434
|
-
return res.die.status301(url, headers);
|
|
435
|
-
return res.die.status404(data, headers);
|
|
436
|
-
return res.die.cors();
|
|
437
|
-
return res.die.fileBase64(fileBase64, mimeType, headers);
|
|
438
|
-
return await res.die.file(filePath, headers, fallbackFilePath);
|
|
439
|
-
return await res.die.ejsFile(filePath, pageData, headers);
|
|
440
|
-
return await res.die.ejsTemplate(template, pageData, headers);
|
|
441
|
-
return res.die.api(payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage }, headers);
|
|
442
|
-
});
|
|
443
|
-
```
|
|
384
|
+
return res.status404(data, headers);
|
|
385
|
+
// Sends a 404 Not Found response.
|
|
444
386
|
|
|
445
|
-
|
|
387
|
+
return res.cors();
|
|
388
|
+
// Sends a 200 OK response with CORS headers (for preflight requests).
|
|
446
389
|
|
|
447
|
-
|
|
390
|
+
return res.fileBase64(fileBase64, mimeType, headers);
|
|
391
|
+
// Sends a file response from base64 content.
|
|
448
392
|
|
|
449
|
-
|
|
393
|
+
return res.file(filePath, headers, fallbackFilePath);
|
|
394
|
+
// Serves a file from the public directory.
|
|
450
395
|
|
|
451
|
-
|
|
396
|
+
return await res.ejsFile(filePath, pageData, headers);
|
|
397
|
+
// Renders and serves an EJS file.
|
|
452
398
|
|
|
453
|
-
|
|
399
|
+
return await res.ejsTemplate(template, pageData, headers);
|
|
400
|
+
// Renders and serves an EJS template string.
|
|
401
|
+
|
|
402
|
+
return res.api(payload, config, headers);
|
|
403
|
+
// Sends a standardized API response for use with LambderCaller.
|
|
404
|
+
// Config: { notAuthorized, message, errorMessage, versionExpired, sessionExpired }
|
|
454
405
|
|
|
455
|
-
|
|
406
|
+
// res.die.* - Same as res.* but immediately returns and skips afterRender hooks
|
|
407
|
+
return res.die.json(data, headers);
|
|
408
|
+
return res.die.api(payload, config, headers);
|
|
409
|
+
// ... etc
|
|
410
|
+
```
|
|
456
411
|
|
|
457
|
-
|
|
412
|
+
## Frontend Usage with LambderCaller
|
|
413
|
+
|
|
414
|
+
LambderCaller is a frontend companion library for Lambder (only 2kb compressed) designed to simplify making type-safe API requests to your Lambder backend.
|
|
415
|
+
|
|
416
|
+
### Basic Setup with Type Safety
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
458
419
|
import { LambderCaller } from "lambder";
|
|
420
|
+
import type { AppContract } from "./backend/handler"; // Import the inferred contract type
|
|
459
421
|
|
|
460
|
-
const lambderCaller = new LambderCaller({
|
|
422
|
+
const lambderCaller = new LambderCaller<AppContract>({
|
|
423
|
+
apiPath: "/api",
|
|
461
424
|
isCorsEnabled: false,
|
|
462
|
-
apiPath: "/secure", // Your Lambder API endpoint, must be the same as in your backend
|
|
463
425
|
fetchStartedHandler: ({ fetchParams, activeFetchList }) => {
|
|
464
|
-
// When any api call starts
|
|
465
426
|
console.log("API Called:", fetchParams.apiName);
|
|
466
427
|
},
|
|
467
428
|
fetchEndedHandler: ({ fetchParams, fetchResult, activeFetchList }) => {
|
|
468
|
-
|
|
469
|
-
console.log("Ongoing api call count:", activeFetchList.length);
|
|
429
|
+
console.log("Ongoing calls:", activeFetchList.length);
|
|
470
430
|
},
|
|
471
431
|
errorMessageHandler: (message) => {
|
|
472
|
-
console.error("LambderCaller:", message);
|
|
432
|
+
console.error("LambderCaller:", message);
|
|
473
433
|
},
|
|
474
434
|
});
|
|
475
435
|
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
} catch (error) {
|
|
483
|
-
console.error("Failed to fetch page data:", error);
|
|
484
|
-
}
|
|
485
|
-
};
|
|
436
|
+
// Fully typed API calls!
|
|
437
|
+
const user = await lambderCaller.api("getCompanyPage", { companyName: "Acme" });
|
|
438
|
+
// TypeScript knows:
|
|
439
|
+
// - Available API names (autocomplete)
|
|
440
|
+
// - Required input type
|
|
441
|
+
// - Expected output type
|
|
486
442
|
```
|
|
487
443
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
Want compile-time type checking for your APIs? It's incredibly simple!
|
|
491
|
-
|
|
492
|
-
### 1. Define Your API Contract
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
// shared/apiContract.ts
|
|
496
|
-
import type { ApiContract } from 'lambder';
|
|
497
|
-
|
|
498
|
-
export type MyApiContract = ApiContract<{
|
|
499
|
-
getUserById: { input: { userId: string }, output: User },
|
|
500
|
-
createUser: { input: CreateUserInput, output: User },
|
|
501
|
-
listUsers: { input: void, output: User[] }
|
|
502
|
-
}>;
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### 2. Backend - Pass Type to Constructor
|
|
444
|
+
### How Type Safety Works
|
|
506
445
|
|
|
446
|
+
1. **Backend**: Chain your APIs and export the inferred contract
|
|
507
447
|
```typescript
|
|
448
|
+
// backend/handler.ts
|
|
508
449
|
import Lambder from 'lambder';
|
|
509
|
-
import
|
|
510
|
-
|
|
511
|
-
const lambder = new Lambder
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
450
|
+
import { z } from 'zod';
|
|
451
|
+
|
|
452
|
+
const lambder = new Lambder({ apiPath: '/api' })
|
|
453
|
+
.addApi('getUser', {
|
|
454
|
+
input: z.object({ userId: z.string() }),
|
|
455
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
456
|
+
}, async (ctx, res) => {
|
|
457
|
+
return res.api({ id: ctx.apiPayload.userId, name: "John" });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
export type AppContract = typeof lambder.ApiContractType;
|
|
461
|
+
export const handler = lambder.getHandler();
|
|
519
462
|
```
|
|
520
463
|
|
|
521
|
-
|
|
522
|
-
|
|
464
|
+
2. **Frontend**: Import the **type** (not the code) and use it
|
|
523
465
|
```typescript
|
|
466
|
+
// frontend/api.ts
|
|
524
467
|
import { LambderCaller } from 'lambder';
|
|
525
|
-
import type {
|
|
468
|
+
import type { AppContract } from '../backend/handler'; // Type-only import
|
|
526
469
|
|
|
527
|
-
const
|
|
470
|
+
const lambderCaller = new LambderCaller<AppContract>({ apiPath: '/api' });
|
|
528
471
|
|
|
529
|
-
//
|
|
530
|
-
const user = await
|
|
531
|
-
// ↑ IDE shows all available APIs
|
|
532
|
-
// ↑ Type-checked input
|
|
533
|
-
// user is typed as User | null | undefined
|
|
472
|
+
// ✅ Fully typed - TypeScript knows all APIs and their input/output types
|
|
473
|
+
const user = await lambderCaller.api('getUser', { userId: '123' });
|
|
534
474
|
```
|
|
535
475
|
|
|
536
476
|
### Benefits
|
|
537
477
|
|
|
538
|
-
✅ **
|
|
478
|
+
✅ **No Manual Type Definitions** - Types are inferred from your Zod schemas
|
|
479
|
+
✅ **Single Source of Truth** - API contract comes from your backend code
|
|
480
|
+
✅ **Runtime Validation** - Zod validates inputs automatically
|
|
481
|
+
✅ **Compile-Time Safety** - TypeScript catches errors before runtime
|
|
539
482
|
✅ **Autocomplete** - IDE suggests available APIs as you type
|
|
540
|
-
✅ **
|
|
541
|
-
✅ **No Wrappers** - Use existing `api()` and `addApi()` methods
|
|
542
|
-
✅ **Opt-In** - Add when you want, skip when you don't
|
|
543
|
-
✅ **Zero Overhead** - Pure TypeScript types, no runtime code
|
|
483
|
+
✅ **Zero Overhead** - Type-only imports, no runtime code bloat
|
|
544
484
|
|
|
545
485
|
📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
|
|
546
486
|
|
|
547
487
|
## Testing with LambderMSW
|
|
548
488
|
|
|
549
|
-
LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your APIs
|
|
489
|
+
LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your APIs with full type safety.
|
|
550
490
|
|
|
551
491
|
```typescript
|
|
552
492
|
import { LambderMSW } from 'lambder';
|
|
553
493
|
import { setupServer } from 'msw/node';
|
|
554
|
-
import type {
|
|
494
|
+
import type { AppContract } from './backend/handler';
|
|
555
495
|
|
|
556
|
-
const lambderMSW = new LambderMSW<
|
|
496
|
+
const lambderMSW = new LambderMSW<AppContract>({
|
|
557
497
|
apiPath: '/api',
|
|
558
498
|
});
|
|
559
499
|
|
|
560
500
|
const handlers = [
|
|
561
501
|
// Mock API with full type safety! ✨
|
|
562
|
-
lambderMSW.mockApi('
|
|
502
|
+
lambderMSW.mockApi('getUser', async (payload) => {
|
|
503
|
+
// payload is typed based on your Zod schema
|
|
563
504
|
return {
|
|
564
505
|
id: payload.userId,
|
|
565
506
|
name: 'John Doe',
|
|
@@ -567,11 +508,15 @@ const handlers = [
|
|
|
567
508
|
};
|
|
568
509
|
}),
|
|
569
510
|
|
|
570
|
-
//
|
|
511
|
+
// Simulate delays and custom responses
|
|
571
512
|
lambderMSW.mockApi('createUser', async (payload) => {
|
|
572
|
-
return { id: '123',
|
|
573
|
-
}, {
|
|
513
|
+
return { id: '123', name: payload.name, email: payload.email };
|
|
514
|
+
}, {
|
|
515
|
+
delay: 500,
|
|
516
|
+
message: 'User created successfully'
|
|
517
|
+
}),
|
|
574
518
|
|
|
519
|
+
// Mock session expired
|
|
575
520
|
lambderMSW.mockSessionExpired('protectedApi'),
|
|
576
521
|
];
|
|
577
522
|
|