lambder 1.0.128 → 1.0.130
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/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +90 -0
- package/Readme.md +49 -0
- package/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- package/dist/LambderMSW.d.ts +104 -0
- package/dist/LambderMSW.js +203 -0
- package/dist/LambderResolver.d.ts +17 -15
- package/dist/LambderResolver.js +4 -0
- package/dist/LambderResponseBuilder.d.ts +2 -1
- package/dist/LambderSessionController.d.ts +1 -0
- package/dist/LambderSessionController.js +10 -0
- package/dist/LambderSessionManager.d.ts +6 -1
- package/dist/LambderSessionManager.js +43 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/MSW_MOCKING.md +590 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- package/examples/msw-mocking-example.ts +241 -0
- package/examples/output-type-enforcement-example.ts +218 -0
- package/examples/secure-session-example.ts +191 -0
- package/examples/test-output-type-enforcement.ts +101 -0
- package/package.json +9 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderMSW.ts +330 -0
- package/src/LambderResolver.ts +32 -15
- package/src/LambderResponseBuilder.ts +2 -1
- package/src/LambderSessionController.ts +9 -0
- package/src/LambderSessionManager.ts +51 -10
- package/src/index.ts +11 -0
- package/tests/msw-integration.test.ts +183 -0
- package/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Output Type Enforcement - Implementation Summary
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
The type system was only enforcing input types for `ctx.apiPayload`, but not enforcing that the output returned by `resolver.api()` matched the contract's output type.
|
|
5
|
+
|
|
6
|
+
## Solution
|
|
7
|
+
Made the following components generic to propagate type information:
|
|
8
|
+
|
|
9
|
+
### 1. LambderResponseBuilder
|
|
10
|
+
- Added generic parameter: `LambderResponseBuilder<TContract extends ApiContractShape = any>`
|
|
11
|
+
- The `api()` method now uses the generic type for type checking
|
|
12
|
+
|
|
13
|
+
### 2. LambderResolver
|
|
14
|
+
- Added generic parameters: `LambderResolver<TContract extends ApiContractShape = any, TApiName extends keyof TContract & string = any>`
|
|
15
|
+
- Overrides the `api()` method to enforce output type: `api(payload: ApiOutput<TContract, TApiName> | null, ...)`
|
|
16
|
+
- The `die.api()` method also enforces the output type through the interface
|
|
17
|
+
|
|
18
|
+
### 3. Lambder addApi/addSessionApi
|
|
19
|
+
- Updated type signatures to pass `LambderResolver<TContract, TApiName>` to the action function
|
|
20
|
+
- Now both methods enforce:
|
|
21
|
+
- ✅ Input type via `ctx.apiPayload: TContract[TApiName]['input']`
|
|
22
|
+
- ✅ Output type via `resolver.api(payload: TContract[TApiName]['output'] | null)`
|
|
23
|
+
|
|
24
|
+
## Type Flow
|
|
25
|
+
```
|
|
26
|
+
Contract Definition
|
|
27
|
+
↓
|
|
28
|
+
Lambder<TContract>
|
|
29
|
+
↓
|
|
30
|
+
addApi<TApiName>(apiName, actionFn)
|
|
31
|
+
↓
|
|
32
|
+
actionFn(ctx: LambderRenderContext<Input>, resolver: LambderResolver<TContract, TApiName>)
|
|
33
|
+
↓
|
|
34
|
+
resolver.api(payload: Output | null)
|
|
35
|
+
↓
|
|
36
|
+
Type validation at compile time!
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What's Enforced Now
|
|
40
|
+
|
|
41
|
+
### Input Types (already working)
|
|
42
|
+
```typescript
|
|
43
|
+
lambder.addApi('getUserById', async (ctx, resolver) => {
|
|
44
|
+
ctx.apiPayload.userId; // ✅ TypeScript knows this is string
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Output Types (NEW!)
|
|
49
|
+
```typescript
|
|
50
|
+
lambder.addApi('getUserById', async (ctx, resolver) => {
|
|
51
|
+
const user = { id: "123", name: "John", age: 30 };
|
|
52
|
+
return resolver.api(user); // ✅ TypeScript validates user matches User type
|
|
53
|
+
|
|
54
|
+
// return resolver.api("wrong"); // ❌ TypeScript error!
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
- ✅ Works with `resolver.api()`
|
|
60
|
+
- ✅ Works with `resolver.die.api()`
|
|
61
|
+
- ✅ Works with `addApi()`
|
|
62
|
+
- ✅ Works with `addSessionApi()`
|
|
63
|
+
- ✅ Supports nullable outputs (`output: User | null`)
|
|
64
|
+
- ✅ Supports complex nested types
|
|
65
|
+
- ✅ Supports arrays
|
|
66
|
+
- ✅ Supports primitive types
|
|
67
|
+
- ✅ Zero runtime overhead - all validation is at compile time
|
|
68
|
+
- ✅ Backward compatible - untyped APIs still work
|
|
69
|
+
|
|
70
|
+
## Files Changed
|
|
71
|
+
1. `src/LambderResponseBuilder.ts` - Made generic, imported ApiContractShape
|
|
72
|
+
2. `src/LambderResolver.ts` - Made generic, added typed `api()` override
|
|
73
|
+
3. `src/Lambder.ts` - Updated addApi/addSessionApi type signatures
|
|
74
|
+
4. `docs/TYPE_SAFE_QUICK_START.md` - Updated documentation with output type examples
|
|
75
|
+
|
|
76
|
+
## Examples Created
|
|
77
|
+
1. `examples/test-output-type-enforcement.ts` - Unit test for type enforcement
|
|
78
|
+
2. `examples/output-type-enforcement-example.ts` - Comprehensive demonstration
|
|
79
|
+
|
|
80
|
+
## Testing
|
|
81
|
+
All existing examples and tests compile without errors:
|
|
82
|
+
- ✅ `examples/simplified-typed-api-example.ts`
|
|
83
|
+
- ✅ `tests/type-safety.test.ts`
|
|
84
|
+
- ✅ No TypeScript compilation errors in project
|
|
85
|
+
|
|
86
|
+
## Backward Compatibility
|
|
87
|
+
The changes are fully backward compatible:
|
|
88
|
+
- Untyped usage still works: `new Lambder({ ... })` without generic
|
|
89
|
+
- Typed usage is opt-in: `new Lambder<MyContract>({ ... })`
|
|
90
|
+
- Existing code continues to work unchanged
|
package/Readme.md
CHANGED
|
@@ -5,9 +5,11 @@ Lambder is a highly opinionated dynamic serverless framework designed to facilit
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Simple API & Route Declaration**: Define your APIs and routes using concise and expressive syntax.
|
|
8
|
+
- **Type-Safe API Contracts**: Full TypeScript type safety across frontend and backend with zero runtime overhead.
|
|
8
9
|
- **Session Management**: Built-in session management to secure and personalize user experiences.
|
|
9
10
|
- **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle, enabling fine-grained control over the application flow.
|
|
10
11
|
- **Error Handling**: Comprehensive error handling capabilities, including global error handlers and route-specific fallbacks.
|
|
12
|
+
- **MSW Mocking Support**: Optional Mock Service Worker integration for easy API mocking in tests and development.
|
|
11
13
|
- **Seamless Integration**: Designed to work effortlessly with AWS Lambda and API Gateway, providing a straightforward path to deploy serverless applications.
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
@@ -544,6 +546,53 @@ const user = await caller.api('getUserById', { userId: '123' });
|
|
|
544
546
|
|
|
545
547
|
📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
|
|
546
548
|
|
|
549
|
+
## Testing & Mocking with MSW
|
|
550
|
+
|
|
551
|
+
Lambder provides optional integration with [Mock Service Worker (MSW)](https://mswjs.io/) for easy API mocking in tests and development.
|
|
552
|
+
|
|
553
|
+
### Installation
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
npm install --save-dev msw
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Quick Example
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { mockLambderApi } from 'lambder';
|
|
563
|
+
import { setupServer } from 'msw/node';
|
|
564
|
+
import type { MyApiContract } from './apiContract';
|
|
565
|
+
|
|
566
|
+
// Create type-safe mock handlers
|
|
567
|
+
const handlers = [
|
|
568
|
+
mockLambderApi<MyApiContract, 'getUserById'>(
|
|
569
|
+
'getUserById',
|
|
570
|
+
(input) => ({
|
|
571
|
+
id: input.userId,
|
|
572
|
+
name: 'Test User',
|
|
573
|
+
email: 'test@example.com'
|
|
574
|
+
})
|
|
575
|
+
)
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
// Set up MSW server
|
|
579
|
+
const server = setupServer(...handlers);
|
|
580
|
+
|
|
581
|
+
beforeAll(() => server.listen());
|
|
582
|
+
afterEach(() => server.resetHandlers());
|
|
583
|
+
afterAll(() => server.close());
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Benefits
|
|
587
|
+
|
|
588
|
+
✅ **Type-Safe Mocking** - Full TypeScript support for mock responses
|
|
589
|
+
✅ **Easy Setup** - Simple functions that match your API contract
|
|
590
|
+
✅ **Test Scenarios** - Mock errors, session expired, not authorized, etc.
|
|
591
|
+
✅ **Development Mode** - Use mocks while developing frontend features
|
|
592
|
+
✅ **Storybook Ready** - Perfect for component stories
|
|
593
|
+
|
|
594
|
+
📖 **[Read the MSW Mocking Guide](docs/MSW_MOCKING.md)** for detailed setup and examples!
|
|
595
|
+
|
|
547
596
|
## Contributing
|
|
548
597
|
|
|
549
598
|
Contributions are welcome! Especially for documentation. If you have an idea for an improvement or have found a bug, please open an issue or submit a pull request.
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -66,10 +66,11 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
66
66
|
apiVersion?: string;
|
|
67
67
|
});
|
|
68
68
|
enableCors(isCorsEnabled: boolean): void;
|
|
69
|
-
enableDdbSession({ tableName, tableRegion, sessionSalt }: {
|
|
69
|
+
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: {
|
|
70
70
|
tableName: string;
|
|
71
71
|
tableRegion: string;
|
|
72
72
|
sessionSalt: string;
|
|
73
|
+
enableSlidingExpiration?: boolean;
|
|
73
74
|
}, { partitionKey, sortKey }?: {
|
|
74
75
|
partitionKey: string;
|
|
75
76
|
sortKey: string;
|
|
@@ -88,17 +89,17 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
88
89
|
addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
89
90
|
addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
90
91
|
addApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
91
|
-
addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
92
|
+
addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver<TContract, TApiName>) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
92
93
|
addApi(apiName: string, actionFn: ActionFunction): void;
|
|
93
94
|
addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
94
|
-
addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
95
|
+
addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver<TContract, TApiName>) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
95
96
|
addSessionApi(apiName: string, actionFn: ActionFunction): void;
|
|
96
97
|
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
97
98
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
98
99
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
99
100
|
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
100
101
|
getSessionController(ctx: LambderRenderContext<any>): LambderSessionController;
|
|
101
|
-
getResponseBuilder(): LambderResponseBuilder
|
|
102
|
+
getResponseBuilder(): LambderResponseBuilder<any>;
|
|
102
103
|
private getResolver;
|
|
103
104
|
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
|
|
104
105
|
}
|
package/dist/Lambder.js
CHANGED
|
@@ -75,9 +75,9 @@ export default class Lambder {
|
|
|
75
75
|
enableCors(isCorsEnabled) {
|
|
76
76
|
this.isCorsEnabled = isCorsEnabled;
|
|
77
77
|
}
|
|
78
|
-
enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
|
|
78
|
+
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
|
|
79
79
|
this.lambderSessionManager = new LambderSessionManager({
|
|
80
|
-
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
80
|
+
tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
setSessionCookieKey(sessionTokenCookieKey, sessionCsrfCookieKey) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ApiContractShape } from './LambderApiContract.js';
|
|
2
|
+
export type HttpHandler = (info: any) => Promise<Response | undefined> | Response | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mock handler for a Lambder API endpoint
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* mockLambderApi('public.getInitialPageData', () => ({
|
|
9
|
+
* userLocationData: { ... },
|
|
10
|
+
* sessionUser: null
|
|
11
|
+
* }))
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @example with dynamic response based on input
|
|
15
|
+
* ```typescript
|
|
16
|
+
* mockLambderApi('user.updateProfile', (input) => ({
|
|
17
|
+
* success: true,
|
|
18
|
+
* userId: input.userId
|
|
19
|
+
* }))
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function mockLambderApi<TContract extends ApiContractShape, TApiName extends keyof TContract & string>(apiName: TApiName, responseFactory: (input: TContract[TApiName]['input']) => TContract[TApiName]['output'] | Promise<TContract[TApiName]['output']>, options?: {
|
|
23
|
+
/** Custom API path, defaults to '/api' */
|
|
24
|
+
apiPath?: string;
|
|
25
|
+
/** Delay in ms before responding (for testing loading states) */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Custom apiVersion to include in response */
|
|
28
|
+
apiVersion?: string | null;
|
|
29
|
+
}): HttpHandler;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a mock handler that returns an error for a Lambder API endpoint
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* mockLambderApiError('user.deleteAccount', 'Account deletion failed')
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function mockLambderApiError<TContract extends ApiContractShape, TApiName extends keyof TContract & string>(apiName: TApiName, errorMessage: string, options?: {
|
|
39
|
+
/** Custom API path, defaults to '/api' */
|
|
40
|
+
apiPath?: string;
|
|
41
|
+
/** Delay in ms before responding */
|
|
42
|
+
delay?: number;
|
|
43
|
+
/** Custom apiVersion to include in response */
|
|
44
|
+
apiVersion?: string | null;
|
|
45
|
+
}): HttpHandler;
|
|
46
|
+
/**
|
|
47
|
+
* Creates a mock handler that simulates a session expired error
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* mockLambderSessionExpired('user.updateProfile')
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function mockLambderSessionExpired<TContract extends ApiContractShape, TApiName extends keyof TContract & string>(apiName: TApiName, options?: {
|
|
55
|
+
/** Custom API path, defaults to '/api' */
|
|
56
|
+
apiPath?: string;
|
|
57
|
+
/** Custom apiVersion to include in response */
|
|
58
|
+
apiVersion?: string | null;
|
|
59
|
+
}): HttpHandler;
|
|
60
|
+
/**
|
|
61
|
+
* Creates a mock handler that simulates a "not authorized" error
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* mockLambderNotAuthorized('admin.deleteUser')
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function mockLambderNotAuthorized<TContract extends ApiContractShape, TApiName extends keyof TContract & string>(apiName: TApiName, options?: {
|
|
69
|
+
/** Custom API path, defaults to '/api' */
|
|
70
|
+
apiPath?: string;
|
|
71
|
+
/** Custom apiVersion to include in response */
|
|
72
|
+
apiVersion?: string | null;
|
|
73
|
+
}): HttpHandler;
|
|
74
|
+
/**
|
|
75
|
+
* Creates a mock handler that simulates a version expired error
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* mockLambderVersionExpired('user.updateProfile')
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function mockLambderVersionExpired<TContract extends ApiContractShape, TApiName extends keyof TContract & string>(apiName: TApiName, options?: {
|
|
83
|
+
/** Custom API path, defaults to '/api' */
|
|
84
|
+
apiPath?: string;
|
|
85
|
+
/** Custom apiVersion to include in response */
|
|
86
|
+
apiVersion?: string | null;
|
|
87
|
+
}): HttpHandler;
|
|
88
|
+
/**
|
|
89
|
+
* Helper function to create the MSW http.post handler with proper path matching
|
|
90
|
+
* This is useful when you need to set up multiple handlers with the same path
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { setupServer } from 'msw/node';
|
|
95
|
+
*
|
|
96
|
+
* const server = setupServer(
|
|
97
|
+
* createLambderApiHandler('/api', [
|
|
98
|
+
* mockLambderApi('user.getProfile', () => ({ name: 'Test User' })),
|
|
99
|
+
* mockLambderApi('user.updateProfile', (input) => ({ success: true }))
|
|
100
|
+
* ])
|
|
101
|
+
* );
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare function createLambderApiHandler(apiPath: string, handlers: HttpHandler[]): HttpHandler;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Dynamic imports to avoid requiring MSW as a hard dependency
|
|
2
|
+
let http;
|
|
3
|
+
let HttpResponse;
|
|
4
|
+
/**
|
|
5
|
+
* Initialize MSW dependencies dynamically
|
|
6
|
+
* This allows the module to be imported even if MSW is not installed
|
|
7
|
+
*/
|
|
8
|
+
async function ensureMSW() {
|
|
9
|
+
if (!http || !HttpResponse) {
|
|
10
|
+
try {
|
|
11
|
+
// @ts-ignore - MSW is an optional peer dependency
|
|
12
|
+
const msw = await import('msw');
|
|
13
|
+
http = msw.http;
|
|
14
|
+
HttpResponse = msw.HttpResponse;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
throw new Error('MSW is required to use Lambder mocking utilities. ' +
|
|
18
|
+
'Install it with: npm install --save-dev msw');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a mock handler for a Lambder API endpoint
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* mockLambderApi('public.getInitialPageData', () => ({
|
|
28
|
+
* userLocationData: { ... },
|
|
29
|
+
* sessionUser: null
|
|
30
|
+
* }))
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example with dynamic response based on input
|
|
34
|
+
* ```typescript
|
|
35
|
+
* mockLambderApi('user.updateProfile', (input) => ({
|
|
36
|
+
* success: true,
|
|
37
|
+
* userId: input.userId
|
|
38
|
+
* }))
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function mockLambderApi(apiName, responseFactory, options) {
|
|
42
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
43
|
+
const delay = options?.delay ?? 0;
|
|
44
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
45
|
+
// Return a handler that will be initialized when MSW is available
|
|
46
|
+
return (async ({ request }) => {
|
|
47
|
+
await ensureMSW();
|
|
48
|
+
const body = (await request.json());
|
|
49
|
+
// Only handle this specific API
|
|
50
|
+
if (body.apiName !== apiName) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log(`[MSW Lambder] Mocking ${apiName}`, body.payload);
|
|
54
|
+
// Apply delay if specified
|
|
55
|
+
if (delay > 0) {
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
57
|
+
}
|
|
58
|
+
// Generate the response
|
|
59
|
+
const payload = await responseFactory(body.payload);
|
|
60
|
+
// Return in Lambder API response format
|
|
61
|
+
return HttpResponse.json({
|
|
62
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
63
|
+
payload,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates a mock handler that returns an error for a Lambder API endpoint
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* mockLambderApiError('user.deleteAccount', 'Account deletion failed')
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function mockLambderApiError(apiName, errorMessage, options) {
|
|
76
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
77
|
+
const delay = options?.delay ?? 0;
|
|
78
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
79
|
+
return (async ({ request }) => {
|
|
80
|
+
await ensureMSW();
|
|
81
|
+
const body = (await request.json());
|
|
82
|
+
if (body.apiName !== apiName) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(`[MSW Lambder] Mocking error for ${apiName}:`, errorMessage);
|
|
86
|
+
if (delay > 0) {
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
88
|
+
}
|
|
89
|
+
return HttpResponse.json({
|
|
90
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
91
|
+
payload: null,
|
|
92
|
+
errorMessage,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates a mock handler that simulates a session expired error
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* mockLambderSessionExpired('user.updateProfile')
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function mockLambderSessionExpired(apiName, options) {
|
|
105
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
106
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
107
|
+
return (async ({ request }) => {
|
|
108
|
+
await ensureMSW();
|
|
109
|
+
const body = (await request.json());
|
|
110
|
+
if (body.apiName !== apiName) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log(`[MSW Lambder] Mocking session expired for ${apiName}`);
|
|
114
|
+
return HttpResponse.json({
|
|
115
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
116
|
+
payload: null,
|
|
117
|
+
sessionExpired: true,
|
|
118
|
+
errorMessage: 'Session expired',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Creates a mock handler that simulates a "not authorized" error
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* mockLambderNotAuthorized('admin.deleteUser')
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function mockLambderNotAuthorized(apiName, options) {
|
|
131
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
132
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
133
|
+
return (async ({ request }) => {
|
|
134
|
+
await ensureMSW();
|
|
135
|
+
const body = (await request.json());
|
|
136
|
+
if (body.apiName !== apiName) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(`[MSW Lambder] Mocking not authorized for ${apiName}`);
|
|
140
|
+
return HttpResponse.json({
|
|
141
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
142
|
+
payload: null,
|
|
143
|
+
notAuthorized: true,
|
|
144
|
+
errorMessage: 'Not authorized',
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Creates a mock handler that simulates a version expired error
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* mockLambderVersionExpired('user.updateProfile')
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function mockLambderVersionExpired(apiName, options) {
|
|
157
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
158
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
159
|
+
return (async ({ request }) => {
|
|
160
|
+
await ensureMSW();
|
|
161
|
+
const body = (await request.json());
|
|
162
|
+
if (body.apiName !== apiName) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
console.log(`[MSW Lambder] Mocking version expired for ${apiName}`);
|
|
166
|
+
return HttpResponse.json({
|
|
167
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
168
|
+
payload: null,
|
|
169
|
+
versionExpired: true,
|
|
170
|
+
errorMessage: 'Version expired',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Helper function to create the MSW http.post handler with proper path matching
|
|
176
|
+
* This is useful when you need to set up multiple handlers with the same path
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* import { setupServer } from 'msw/node';
|
|
181
|
+
*
|
|
182
|
+
* const server = setupServer(
|
|
183
|
+
* createLambderApiHandler('/api', [
|
|
184
|
+
* mockLambderApi('user.getProfile', () => ({ name: 'Test User' })),
|
|
185
|
+
* mockLambderApi('user.updateProfile', (input) => ({ success: true }))
|
|
186
|
+
* ])
|
|
187
|
+
* );
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function createLambderApiHandler(apiPath, handlers) {
|
|
191
|
+
return (async (info) => {
|
|
192
|
+
await ensureMSW();
|
|
193
|
+
// Call each handler in sequence until one handles the request
|
|
194
|
+
for (const handler of handlers) {
|
|
195
|
+
const result = await handler(info);
|
|
196
|
+
if (result) {
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// No handler matched
|
|
201
|
+
return;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./Lambder.js";
|
|
2
2
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
3
3
|
import LambderUtils from "./LambderUtils.js";
|
|
4
|
+
import type { ApiContractShape, ApiOutput } from "./LambderApiContract.js";
|
|
4
5
|
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
5
|
-
interface DieResolverMethods {
|
|
6
|
-
raw: MethodType<LambderResponseBuilder
|
|
7
|
-
json: MethodType<LambderResponseBuilder
|
|
8
|
-
xml: MethodType<LambderResponseBuilder
|
|
9
|
-
html: MethodType<LambderResponseBuilder
|
|
10
|
-
status301: MethodType<LambderResponseBuilder
|
|
11
|
-
status404: MethodType<LambderResponseBuilder
|
|
12
|
-
cors: MethodType<LambderResponseBuilder
|
|
13
|
-
fileBase64: MethodType<LambderResponseBuilder
|
|
14
|
-
file: MethodType<LambderResponseBuilder
|
|
15
|
-
ejsFile: MethodType<LambderResponseBuilder
|
|
16
|
-
ejsTemplate: MethodType<LambderResponseBuilder
|
|
17
|
-
api:
|
|
6
|
+
interface DieResolverMethods<TContract extends ApiContractShape, TApiName extends keyof TContract & string> {
|
|
7
|
+
raw: MethodType<LambderResponseBuilder<TContract>, 'raw'>;
|
|
8
|
+
json: MethodType<LambderResponseBuilder<TContract>, 'json'>;
|
|
9
|
+
xml: MethodType<LambderResponseBuilder<TContract>, 'xml'>;
|
|
10
|
+
html: MethodType<LambderResponseBuilder<TContract>, 'html'>;
|
|
11
|
+
status301: MethodType<LambderResponseBuilder<TContract>, 'status301'>;
|
|
12
|
+
status404: MethodType<LambderResponseBuilder<TContract>, 'status404'>;
|
|
13
|
+
cors: MethodType<LambderResponseBuilder<TContract>, 'cors'>;
|
|
14
|
+
fileBase64: MethodType<LambderResponseBuilder<TContract>, 'fileBase64'>;
|
|
15
|
+
file: MethodType<LambderResponseBuilder<TContract>, 'file'>;
|
|
16
|
+
ejsFile: MethodType<LambderResponseBuilder<TContract>, 'ejsFile'>;
|
|
17
|
+
ejsTemplate: MethodType<LambderResponseBuilder<TContract>, 'ejsTemplate'>;
|
|
18
|
+
api: (payload: ApiOutput<TContract, TApiName> | null, config?: Parameters<LambderResponseBuilder<TContract>['api']>[1], headers?: Parameters<LambderResponseBuilder<TContract>['api']>[2]) => LambderResolverResponse;
|
|
18
19
|
}
|
|
19
|
-
export default class LambderResolver extends LambderResponseBuilder {
|
|
20
|
+
export default class LambderResolver<TContract extends ApiContractShape = any, TApiName extends keyof TContract & string = any> extends LambderResponseBuilder<TContract> {
|
|
20
21
|
resolve: (response: LambderResolverResponse) => void;
|
|
21
22
|
reject: (err: Error) => void;
|
|
22
|
-
die: DieResolverMethods
|
|
23
|
+
die: DieResolverMethods<TContract, TApiName>;
|
|
23
24
|
constructor({ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, resolve, reject }: {
|
|
24
25
|
isCorsEnabled: boolean;
|
|
25
26
|
publicPath: string;
|
|
@@ -29,6 +30,7 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
29
30
|
resolve: (response: LambderResolverResponse) => void;
|
|
30
31
|
reject: (err: Error) => void;
|
|
31
32
|
});
|
|
33
|
+
api(payload: ApiOutput<TContract, TApiName> | null, config?: Parameters<LambderResponseBuilder<TContract>['api']>[1], headers?: Parameters<LambderResponseBuilder<TContract>['api']>[2]): LambderResolverResponse;
|
|
32
34
|
private autoResolve;
|
|
33
35
|
private autoResolvePromise;
|
|
34
36
|
}
|
package/dist/LambderResolver.js
CHANGED
|
@@ -22,6 +22,10 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
22
22
|
api: this.autoResolve(this.api),
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
// Override api method with proper typing
|
|
26
|
+
api(payload, config, headers) {
|
|
27
|
+
return super.api(payload, config, headers);
|
|
28
|
+
}
|
|
25
29
|
autoResolve(method) {
|
|
26
30
|
return (...args) => {
|
|
27
31
|
const result = method.apply(this, args);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import LambderUtils from "./LambderUtils.js";
|
|
2
2
|
import { LambderRenderContext } from "./Lambder.js";
|
|
3
|
+
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
3
4
|
export type LambderResolverResponse = {
|
|
4
5
|
statusCode: number;
|
|
5
6
|
multiValueHeaders?: Record<string, string[]>;
|
|
@@ -17,7 +18,7 @@ export type LambderApiResponseConfig = {
|
|
|
17
18
|
export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
18
19
|
payload?: T | null;
|
|
19
20
|
};
|
|
20
|
-
export default class LambderResponseBuilder {
|
|
21
|
+
export default class LambderResponseBuilder<TContract extends ApiContractShape = any> {
|
|
21
22
|
private isCorsEnabled;
|
|
22
23
|
private publicPath;
|
|
23
24
|
private apiVersion;
|
|
@@ -14,6 +14,7 @@ export default class LambderSessionController {
|
|
|
14
14
|
});
|
|
15
15
|
private areRequestSessionTokensValid;
|
|
16
16
|
createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
|
|
17
|
+
regenerateSession(): Promise<LambderSessionContext>;
|
|
17
18
|
fetchSession(): Promise<LambderSessionContext>;
|
|
18
19
|
fetchSessionIfExists(): Promise<LambderSessionContext | null>;
|
|
19
20
|
isSessionValid(session: any): boolean;
|
|
@@ -31,6 +31,16 @@ export default class LambderSessionController {
|
|
|
31
31
|
return this.ctx.session;
|
|
32
32
|
}
|
|
33
33
|
;
|
|
34
|
+
async regenerateSession() {
|
|
35
|
+
if (!this.ctx.session)
|
|
36
|
+
throw new Error("Session not found.");
|
|
37
|
+
const newSession = await this.lambderSessionManager.regenerateSession(this.ctx.session);
|
|
38
|
+
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionTokenCookieKey}=${newSession.sessionToken}; Expires=${new Date(newSession.expiresAt * 1000).toUTCString()}; Path=/; HttpOnly; SameSite=Lax; Secure` });
|
|
39
|
+
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionCsrfCookieKey}=${newSession.csrfToken}; Expires=${new Date(newSession.expiresAt * 1000).toUTCString()}; Path=/; SameSite=Lax; Secure` });
|
|
40
|
+
this.ctx.session = newSession;
|
|
41
|
+
return this.ctx.session;
|
|
42
|
+
}
|
|
43
|
+
;
|
|
34
44
|
async fetchSession() {
|
|
35
45
|
if (!this.areRequestSessionTokensValid()) {
|
|
36
46
|
throw new Error("Session tokens are invalid");
|
|
@@ -6,6 +6,7 @@ export type LambderSessionContext = {
|
|
|
6
6
|
data: any;
|
|
7
7
|
createdAt: number;
|
|
8
8
|
expiresAt: number;
|
|
9
|
+
lastAccessedAt: number;
|
|
9
10
|
ttlInSeconds: number;
|
|
10
11
|
};
|
|
11
12
|
export default class LambderSessionManager {
|
|
@@ -14,14 +15,17 @@ export default class LambderSessionManager {
|
|
|
14
15
|
private partitionKey;
|
|
15
16
|
private sortKey;
|
|
16
17
|
private ddbDocumentClient;
|
|
17
|
-
|
|
18
|
+
private enableSlidingExpiration;
|
|
19
|
+
constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration, }: {
|
|
18
20
|
tableName: string;
|
|
19
21
|
tableRegion: string;
|
|
20
22
|
partitionKey: string;
|
|
21
23
|
sortKey: string;
|
|
22
24
|
sessionSalt: string;
|
|
25
|
+
enableSlidingExpiration?: boolean;
|
|
23
26
|
});
|
|
24
27
|
private sessionUserKeyHasher;
|
|
28
|
+
private constantTimeCompare;
|
|
25
29
|
private ddbGetItem;
|
|
26
30
|
private ddbPutItem;
|
|
27
31
|
private ddbDeleteItem;
|
|
@@ -33,4 +37,5 @@ export default class LambderSessionManager {
|
|
|
33
37
|
isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck?: boolean): boolean;
|
|
34
38
|
deleteSession(session: Record<string, any>): Promise<boolean>;
|
|
35
39
|
deleteSessionAll(session: Record<string, any>): Promise<boolean>;
|
|
40
|
+
regenerateSession(session: LambderSessionContext): Promise<LambderSessionContext>;
|
|
36
41
|
}
|