lambder 1.0.126 → 1.0.127
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 +2 -2
- package/deploy +5 -0
- package/dist/Lambder.d.ts +16 -20
- package/dist/LambderApiContract.d.ts +19 -8
- package/dist/LambderCaller.d.ts +2 -2
- package/dist/LambderResolver.d.ts +1 -1
- package/dist/LambderResponseBuilder.d.ts +1 -1
- package/dist/LambderSessionController.d.ts +2 -2
- package/docs/TYPE_SAFE_QUICK_START.md +2 -2
- package/examples/simplified-typed-api-example.ts +2 -2
- package/package.json +5 -3
- package/src/Lambder.ts +27 -27
- package/src/LambderApiContract.ts +20 -8
- package/src/LambderCaller.ts +2 -2
- package/src/LambderResolver.ts +1 -1
- package/src/LambderResponseBuilder.ts +2 -2
- package/src/LambderSessionController.ts +2 -2
- package/tests/type-safety.test.ts +124 -146
package/Readme.md
CHANGED
|
@@ -495,11 +495,11 @@ Want compile-time type checking for your APIs? It's incredibly simple!
|
|
|
495
495
|
// shared/apiContract.ts
|
|
496
496
|
import type { ApiContract } from 'lambder';
|
|
497
497
|
|
|
498
|
-
export type MyApiContract = {
|
|
498
|
+
export type MyApiContract = ApiContract<{
|
|
499
499
|
getUserById: { input: { userId: string }, output: User },
|
|
500
500
|
createUser: { input: CreateUserInput, output: User },
|
|
501
501
|
listUsers: { input: void, output: User[] }
|
|
502
|
-
}
|
|
502
|
+
}>;
|
|
503
503
|
```
|
|
504
504
|
|
|
505
505
|
### 2. Backend - Pass Type to Constructor
|
package/deploy
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
# Exit immediately if a command exits with a non-zero status.
|
|
4
4
|
set -e
|
|
5
5
|
|
|
6
|
+
echo "Running tests..."
|
|
7
|
+
npm run test
|
|
8
|
+
|
|
9
|
+
echo "Tests passed! Proceeding with deployment..."
|
|
10
|
+
|
|
6
11
|
# Update the version, build the project, and publish
|
|
7
12
|
rm -rf ./dist/*
|
|
8
13
|
npm version patch --no-git-tag-version
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
|
|
|
4
4
|
import LambderUtils from "./LambderUtils.js";
|
|
5
5
|
import { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
6
6
|
import LambderSessionController from "./LambderSessionController.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
8
8
|
type Path = `/${string}`;
|
|
9
|
-
export type LambderRenderContext = {
|
|
9
|
+
export type LambderRenderContext<TApiPayload = any> = {
|
|
10
10
|
host: string;
|
|
11
11
|
path: string;
|
|
12
12
|
pathParams: Record<string, any> | null;
|
|
@@ -15,7 +15,7 @@ export type LambderRenderContext = {
|
|
|
15
15
|
post: Record<string, any>;
|
|
16
16
|
cookie: Record<string, any>;
|
|
17
17
|
apiName: string;
|
|
18
|
-
apiPayload:
|
|
18
|
+
apiPayload: TApiPayload;
|
|
19
19
|
headers: APIGatewayProxyEventHeaders;
|
|
20
20
|
session: LambderSessionContext | null;
|
|
21
21
|
event: APIGatewayProxyEvent;
|
|
@@ -34,17 +34,17 @@ export type LambderRenderContext = {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
37
|
-
type ConditionFunction = (ctx: LambderRenderContext) => boolean;
|
|
38
|
-
type ActionFunction = (ctx: LambderRenderContext
|
|
37
|
+
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
38
|
+
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
39
39
|
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
40
|
-
type HookBeforeRenderFunction = (ctx: LambderRenderContext
|
|
41
|
-
type HookAfterRenderFunction = (ctx: LambderRenderContext
|
|
42
|
-
type HookFallbackFunction = (ctx: LambderRenderContext
|
|
43
|
-
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
44
|
-
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext
|
|
45
|
-
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext
|
|
46
|
-
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext
|
|
47
|
-
export default class Lambder<TContract extends
|
|
40
|
+
type HookBeforeRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderRenderContext<any> | Error | Promise<LambderRenderContext<any> | Error>;
|
|
41
|
+
type HookAfterRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse | Error | Promise<LambderResolverResponse | Error>;
|
|
42
|
+
type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => void | Promise<void>;
|
|
43
|
+
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any> | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
44
|
+
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
45
|
+
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
46
|
+
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
|
|
47
|
+
export default class Lambder<TContract extends ApiContractShape = any> {
|
|
48
48
|
apiPath: string;
|
|
49
49
|
apiVersion: null | string;
|
|
50
50
|
isCorsEnabled: boolean;
|
|
@@ -88,20 +88,16 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
88
88
|
addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
89
89
|
addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
90
90
|
addApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
91
|
-
addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext
|
|
92
|
-
apiPayload: TContract[TApiName]['input'];
|
|
93
|
-
}, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
91
|
+
addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
94
92
|
addApi(apiName: string, actionFn: ActionFunction): void;
|
|
95
93
|
addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
|
|
96
|
-
addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext
|
|
97
|
-
apiPayload: TContract[TApiName]['input'];
|
|
98
|
-
}, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
94
|
+
addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
|
|
99
95
|
addSessionApi(apiName: string, actionFn: ActionFunction): void;
|
|
100
96
|
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
101
97
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
102
98
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
103
99
|
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
|
|
104
|
-
getSessionController(ctx: LambderRenderContext): LambderSessionController;
|
|
100
|
+
getSessionController(ctx: LambderRenderContext<any>): LambderSessionController;
|
|
105
101
|
getResponseBuilder(): LambderResponseBuilder;
|
|
106
102
|
private getResolver;
|
|
107
103
|
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
|
|
@@ -25,18 +25,29 @@
|
|
|
25
25
|
*/
|
|
26
26
|
/**
|
|
27
27
|
* Base type for API contracts
|
|
28
|
+
*
|
|
29
|
+
* Use this as a constraint when defining your API contract:
|
|
30
|
+
*
|
|
31
|
+
* export type MyApiContract = {
|
|
32
|
+
* echo: { input: { message: string }, output: { echo: string } }
|
|
33
|
+
* } satisfies ApiContract;
|
|
34
|
+
*
|
|
35
|
+
* Or for backward compatibility without satisfies:
|
|
36
|
+
*
|
|
37
|
+
* export type MyApiContract = ApiContract & {
|
|
38
|
+
* echo: { input: { message: string }, output: { echo: string } }
|
|
39
|
+
* }
|
|
28
40
|
*/
|
|
29
|
-
export type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
41
|
+
export type ApiContractShape = Record<string, {
|
|
42
|
+
input: any;
|
|
43
|
+
output: any;
|
|
44
|
+
}>;
|
|
45
|
+
export type ApiContract<T extends ApiContractShape> = T;
|
|
35
46
|
/**
|
|
36
47
|
* Extract input type from contract for a specific API
|
|
37
48
|
*/
|
|
38
|
-
export type ApiInput<TContract extends
|
|
49
|
+
export type ApiInput<TContract extends ApiContractShape, TApiName extends keyof TContract> = TContract[TApiName]['input'];
|
|
39
50
|
/**
|
|
40
51
|
* Extract output type from contract for a specific API
|
|
41
52
|
*/
|
|
42
|
-
export type ApiOutput<TContract extends
|
|
53
|
+
export type ApiOutput<TContract extends ApiContractShape, TApiName extends keyof TContract> = TContract[TApiName]['output'];
|
package/dist/LambderCaller.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LambderApiResponse } from './LambderResponseBuilder';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ApiContractShape } from './LambderApiContract';
|
|
3
3
|
type VoidFunction = () => void | Promise<void>;
|
|
4
4
|
type FetchTracker = {
|
|
5
5
|
apiName: string;
|
|
@@ -22,7 +22,7 @@ type FetchEndEventHandler = (params: {
|
|
|
22
22
|
}) => void | Promise<void>;
|
|
23
23
|
type ErrorHandler = (err: Error) => void | Promise<void>;
|
|
24
24
|
type MessageHandler = (message: any) => void | Promise<void>;
|
|
25
|
-
export default class LambderCaller<TContract extends
|
|
25
|
+
export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
26
26
|
private isCorsEnabled;
|
|
27
27
|
private apiPath;
|
|
28
28
|
private apiVersion?;
|
|
@@ -25,7 +25,7 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
25
25
|
publicPath: string;
|
|
26
26
|
apiVersion?: string | null;
|
|
27
27
|
lambderUtils: LambderUtils;
|
|
28
|
-
ctx: LambderRenderContext
|
|
28
|
+
ctx: LambderRenderContext<any>;
|
|
29
29
|
resolve: (response: LambderResolverResponse) => void;
|
|
30
30
|
reject: (err: Error) => void;
|
|
31
31
|
});
|
|
@@ -28,7 +28,7 @@ export default class LambderResponseBuilder {
|
|
|
28
28
|
publicPath: string;
|
|
29
29
|
apiVersion?: string | null;
|
|
30
30
|
lambderUtils: LambderUtils;
|
|
31
|
-
ctx?: LambderRenderContext
|
|
31
|
+
ctx?: LambderRenderContext<any>;
|
|
32
32
|
});
|
|
33
33
|
private readPublicFileSync;
|
|
34
34
|
private checkPublicFileExist;
|
|
@@ -5,12 +5,12 @@ export default class LambderSessionController {
|
|
|
5
5
|
lambderSessionManager: LambderSessionManager;
|
|
6
6
|
sessionTokenCookieKey: string;
|
|
7
7
|
sessionCsrfCookieKey: string;
|
|
8
|
-
ctx: LambderRenderContext
|
|
8
|
+
ctx: LambderRenderContext<any>;
|
|
9
9
|
constructor({ lambderSessionManager, sessionTokenCookieKey, sessionCsrfCookieKey, ctx, }: {
|
|
10
10
|
lambderSessionManager: LambderSessionManager;
|
|
11
11
|
sessionTokenCookieKey: string;
|
|
12
12
|
sessionCsrfCookieKey: string;
|
|
13
|
-
ctx: LambderRenderContext
|
|
13
|
+
ctx: LambderRenderContext<any>;
|
|
14
14
|
});
|
|
15
15
|
private areRequestSessionTokensValid;
|
|
16
16
|
createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
// shared/apiContract.ts
|
|
9
9
|
import type { ApiContract } from 'lambder';
|
|
10
10
|
|
|
11
|
-
export type MyApiContract = {
|
|
11
|
+
export type MyApiContract = ApiContract<{
|
|
12
12
|
getUserById: { input: { userId: string }, output: User },
|
|
13
13
|
createUser: { input: CreateUserInput, output: User },
|
|
14
14
|
listUsers: { input: void, output: User[] }
|
|
15
|
-
}
|
|
15
|
+
}>;
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
### 2. Backend - Pass Type to Lambder
|
|
@@ -50,7 +50,7 @@ type LoginOutput = {
|
|
|
50
50
|
// Step 2: Define your API contract (shared between frontend and backend)
|
|
51
51
|
// ============================================================================
|
|
52
52
|
|
|
53
|
-
export type MyApiContract = {
|
|
53
|
+
export type MyApiContract = ApiContract<{
|
|
54
54
|
// API with input and output
|
|
55
55
|
getUserById: { input: { userId: string }, output: User },
|
|
56
56
|
|
|
@@ -68,7 +68,7 @@ export type MyApiContract = {
|
|
|
68
68
|
// API with primitive output
|
|
69
69
|
getUserCount: { input: void, output: number },
|
|
70
70
|
deleteUser: { input: { userId: string }, output: boolean },
|
|
71
|
-
}
|
|
71
|
+
}>;
|
|
72
72
|
|
|
73
73
|
// ============================================================================
|
|
74
74
|
// Step 3: Backend - Pass contract type to Lambder
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.127",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:watch": "vitest",
|
|
10
11
|
"build": "tsc",
|
|
11
12
|
"lint": "eslint . --ext .ts,.tsx --fix"
|
|
12
13
|
},
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
38
39
|
"@typescript-eslint/parser": "^7.2.0",
|
|
39
40
|
"eslint": "^8.57.0",
|
|
40
|
-
"typescript": "^5.
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^3.2.4"
|
|
41
43
|
}
|
|
42
44
|
}
|
package/src/Lambder.ts
CHANGED
|
@@ -8,11 +8,11 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
|
|
|
8
8
|
import LambderUtils from "./LambderUtils.js";
|
|
9
9
|
import LambderSessionManager, { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
10
10
|
import LambderSessionController from "./LambderSessionController.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
12
12
|
|
|
13
13
|
type Path = `/${string}`;
|
|
14
14
|
|
|
15
|
-
export type LambderRenderContext = {
|
|
15
|
+
export type LambderRenderContext<TApiPayload = any> = {
|
|
16
16
|
host: string;
|
|
17
17
|
path: string;
|
|
18
18
|
pathParams: Record<string, any> | null;
|
|
@@ -21,7 +21,7 @@ export type LambderRenderContext = {
|
|
|
21
21
|
post: Record<string, any>;
|
|
22
22
|
cookie: Record<string, any>;
|
|
23
23
|
apiName: string;
|
|
24
|
-
apiPayload:
|
|
24
|
+
apiPayload: TApiPayload;
|
|
25
25
|
headers: APIGatewayProxyEventHeaders;
|
|
26
26
|
session: LambderSessionContext|null;
|
|
27
27
|
event: APIGatewayProxyEvent;
|
|
@@ -36,26 +36,26 @@ export type LambderRenderContext = {
|
|
|
36
36
|
|
|
37
37
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
38
38
|
|
|
39
|
-
type ConditionFunction = (ctx: LambderRenderContext) => boolean;
|
|
40
|
-
type ActionFunction = (ctx: LambderRenderContext
|
|
39
|
+
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
40
|
+
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
41
41
|
type ActionObject = { conditionFn: ConditionFunction, actionFn: ActionFunction };
|
|
42
42
|
|
|
43
43
|
type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback";
|
|
44
44
|
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
45
|
-
type HookBeforeRenderFunction = (ctx: LambderRenderContext
|
|
46
|
-
type HookAfterRenderFunction = (ctx: LambderRenderContext
|
|
47
|
-
type HookFallbackFunction = (ctx: LambderRenderContext
|
|
45
|
+
type HookBeforeRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderRenderContext<any>|Error|Promise<LambderRenderContext<any>|Error>;
|
|
46
|
+
type HookAfterRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse|Error|Promise<LambderResolverResponse|Error>;
|
|
47
|
+
type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => void|Promise<void>;
|
|
48
48
|
|
|
49
|
-
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext
|
|
50
|
-
type RouteFallbackHandlerFunction = (ctx:LambderRenderContext
|
|
51
|
-
type ApiFallbackHandlerFunction = (ctx:LambderRenderContext
|
|
49
|
+
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any>|null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
50
|
+
type RouteFallbackHandlerFunction = (ctx:LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
51
|
+
type ApiFallbackHandlerFunction = (ctx:LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
export const createContext = (
|
|
55
55
|
event: APIGatewayProxyEvent,
|
|
56
56
|
lambdaContext: Context,
|
|
57
57
|
apiPath: string,
|
|
58
|
-
):LambderRenderContext => {
|
|
58
|
+
):LambderRenderContext<any> => {
|
|
59
59
|
const host = event.headers.Host || event.headers.host || "";
|
|
60
60
|
const path = event.path;
|
|
61
61
|
const pathParams = null;
|
|
@@ -93,7 +93,7 @@ export const createContext = (
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
export default class Lambder<TContract extends
|
|
96
|
+
export default class Lambder<TContract extends ApiContractShape = any> {
|
|
97
97
|
public apiPath: string;
|
|
98
98
|
public apiVersion: null|string;
|
|
99
99
|
public isCorsEnabled: boolean = false;
|
|
@@ -171,7 +171,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
171
171
|
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
private async handleNoMatchedAction(ctx: LambderRenderContext
|
|
174
|
+
private async handleNoMatchedAction(ctx: LambderRenderContext<any>, resolver: LambderResolver){
|
|
175
175
|
for(const hook of this.hookList["fallback"]){ await hook.hookFn(ctx, resolver); }
|
|
176
176
|
|
|
177
177
|
const isAPI = ctx.path === this.apiPath;
|
|
@@ -196,7 +196,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
196
196
|
|
|
197
197
|
addRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
198
198
|
this.actionList.push({
|
|
199
|
-
conditionFn: (ctx:LambderRenderContext) => (
|
|
199
|
+
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
200
200
|
ctx.method === "GET" &&
|
|
201
201
|
(
|
|
202
202
|
(typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
@@ -204,7 +204,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
204
204
|
(condition?.constructor == RegExp && condition.test(ctx.path))
|
|
205
205
|
)
|
|
206
206
|
),
|
|
207
|
-
actionFn: async (ctx:LambderRenderContext
|
|
207
|
+
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
208
208
|
if(typeof condition === "string"){
|
|
209
209
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
210
210
|
}else if(condition?.constructor == RegExp){
|
|
@@ -218,7 +218,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
218
218
|
|
|
219
219
|
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
220
220
|
this.actionList.push({
|
|
221
|
-
conditionFn: (ctx:LambderRenderContext) => (
|
|
221
|
+
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
222
222
|
ctx.method === "GET" &&
|
|
223
223
|
(
|
|
224
224
|
(typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
@@ -226,7 +226,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
226
226
|
(condition?.constructor == RegExp && condition.test(ctx.path))
|
|
227
227
|
)
|
|
228
228
|
),
|
|
229
|
-
actionFn: async (ctx:LambderRenderContext
|
|
229
|
+
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
230
230
|
await this.getSessionController(ctx).fetchSession();
|
|
231
231
|
|
|
232
232
|
if(typeof condition === "string"){
|
|
@@ -248,7 +248,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
248
248
|
addApi<TApiName extends keyof TContract & string>(
|
|
249
249
|
apiName: TApiName,
|
|
250
250
|
actionFn: (
|
|
251
|
-
ctx: LambderRenderContext
|
|
251
|
+
ctx: LambderRenderContext<TContract[TApiName]['input']>,
|
|
252
252
|
resolver: LambderResolver
|
|
253
253
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
254
254
|
):void;
|
|
@@ -260,14 +260,14 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
260
260
|
// Implementation
|
|
261
261
|
addApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
262
262
|
this.actionList.push({
|
|
263
|
-
conditionFn: (ctx:LambderRenderContext) => (
|
|
263
|
+
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
264
264
|
!!ctx.apiName && (
|
|
265
265
|
(typeof apiName === "string" && ctx.apiName === apiName) ||
|
|
266
266
|
(typeof apiName === "function" && apiName(ctx)) ||
|
|
267
267
|
(apiName?.constructor == RegExp && apiName.test(ctx.apiName))
|
|
268
268
|
)
|
|
269
269
|
),
|
|
270
|
-
actionFn: async (ctx:LambderRenderContext
|
|
270
|
+
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => await actionFn(ctx, resolver),
|
|
271
271
|
});
|
|
272
272
|
};
|
|
273
273
|
|
|
@@ -280,7 +280,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
280
280
|
addSessionApi<TApiName extends keyof TContract & string>(
|
|
281
281
|
apiName: TApiName,
|
|
282
282
|
actionFn: (
|
|
283
|
-
ctx: LambderRenderContext
|
|
283
|
+
ctx: LambderRenderContext<TContract[TApiName]['input']>,
|
|
284
284
|
resolver: LambderResolver
|
|
285
285
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
286
286
|
):void;
|
|
@@ -292,14 +292,14 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
292
292
|
// Implementation
|
|
293
293
|
addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
294
294
|
this.actionList.push({
|
|
295
|
-
conditionFn: (ctx:LambderRenderContext) => (
|
|
295
|
+
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
296
296
|
!!ctx.apiName && (
|
|
297
297
|
(typeof apiName === "string" && ctx.apiName === apiName) ||
|
|
298
298
|
(typeof apiName === "function" && apiName(ctx)) ||
|
|
299
299
|
(apiName?.constructor == RegExp && apiName.test(ctx.apiName))
|
|
300
300
|
)
|
|
301
301
|
),
|
|
302
|
-
actionFn: async (ctx:LambderRenderContext
|
|
302
|
+
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
303
303
|
await this.getSessionController(ctx).fetchSession();
|
|
304
304
|
return await actionFn(ctx, resolver);
|
|
305
305
|
}
|
|
@@ -323,7 +323,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
getSessionController(ctx: LambderRenderContext): LambderSessionController{
|
|
326
|
+
getSessionController(ctx: LambderRenderContext<any>): LambderSessionController{
|
|
327
327
|
if(!this.lambderSessionManager) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
328
328
|
|
|
329
329
|
return new LambderSessionController({
|
|
@@ -344,7 +344,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
344
344
|
};
|
|
345
345
|
|
|
346
346
|
private getResolver(
|
|
347
|
-
ctx: LambderRenderContext
|
|
347
|
+
ctx: LambderRenderContext<any>,
|
|
348
348
|
resolve: (response: LambderResolverResponse) => void,
|
|
349
349
|
reject: (err: Error) => void
|
|
350
350
|
){
|
|
@@ -361,7 +361,7 @@ export default class Lambder<TContract extends ApiContract = any> {
|
|
|
361
361
|
event: APIGatewayProxyEvent,
|
|
362
362
|
lambdaContext: Context
|
|
363
363
|
): Promise<LambderResolverResponse>{
|
|
364
|
-
let eventRenderContext:LambderRenderContext
|
|
364
|
+
let eventRenderContext:LambderRenderContext<any>|null = null;
|
|
365
365
|
|
|
366
366
|
try {
|
|
367
367
|
let ctx = createContext(event, lambdaContext, this.apiPath);
|
|
@@ -26,19 +26,31 @@
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Base type for API contracts
|
|
29
|
+
*
|
|
30
|
+
* Use this as a constraint when defining your API contract:
|
|
31
|
+
*
|
|
32
|
+
* export type MyApiContract = {
|
|
33
|
+
* echo: { input: { message: string }, output: { echo: string } }
|
|
34
|
+
* } satisfies ApiContract;
|
|
35
|
+
*
|
|
36
|
+
* Or for backward compatibility without satisfies:
|
|
37
|
+
*
|
|
38
|
+
* export type MyApiContract = ApiContract & {
|
|
39
|
+
* echo: { input: { message: string }, output: { echo: string } }
|
|
40
|
+
* }
|
|
29
41
|
*/
|
|
30
|
-
export type
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
export type ApiContractShape = Record<string, {
|
|
43
|
+
input: any;
|
|
44
|
+
output: any;
|
|
45
|
+
}>
|
|
46
|
+
|
|
47
|
+
export type ApiContract<T extends ApiContractShape> = T;
|
|
36
48
|
|
|
37
49
|
/**
|
|
38
50
|
* Extract input type from contract for a specific API
|
|
39
51
|
*/
|
|
40
52
|
export type ApiInput<
|
|
41
|
-
TContract extends
|
|
53
|
+
TContract extends ApiContractShape,
|
|
42
54
|
TApiName extends keyof TContract
|
|
43
55
|
> = TContract[TApiName]['input'];
|
|
44
56
|
|
|
@@ -46,6 +58,6 @@ export type ApiInput<
|
|
|
46
58
|
* Extract output type from contract for a specific API
|
|
47
59
|
*/
|
|
48
60
|
export type ApiOutput<
|
|
49
|
-
TContract extends
|
|
61
|
+
TContract extends ApiContractShape,
|
|
50
62
|
TApiName extends keyof TContract
|
|
51
63
|
> = TContract[TApiName]['output'];
|
package/src/LambderCaller.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Cookies from 'js-cookie';
|
|
2
2
|
import { LambderApiResponse } from './LambderResponseBuilder';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ApiContractShape } from './LambderApiContract';
|
|
4
4
|
|
|
5
5
|
type VoidFunction = ()=>void|Promise<void>;
|
|
6
6
|
type FetchTracker = { apiName: string, done: boolean, fetchEndCalled: boolean };
|
|
@@ -24,7 +24,7 @@ type FetchEndEventHandler = (params: {
|
|
|
24
24
|
type ErrorHandler = (err: Error) => void|Promise<void>;
|
|
25
25
|
type MessageHandler = (message:any) => void|Promise<void>;
|
|
26
26
|
|
|
27
|
-
export default class LambderCaller<TContract extends
|
|
27
|
+
export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
28
28
|
private isCorsEnabled: boolean;
|
|
29
29
|
private apiPath: string;
|
|
30
30
|
private apiVersion?: string;
|
package/src/LambderResolver.ts
CHANGED
|
@@ -31,7 +31,7 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
31
31
|
publicPath: string,
|
|
32
32
|
apiVersion?: string|null,
|
|
33
33
|
lambderUtils: LambderUtils,
|
|
34
|
-
ctx: LambderRenderContext
|
|
34
|
+
ctx: LambderRenderContext<any>,
|
|
35
35
|
resolve: (response: LambderResolverResponse) => void,
|
|
36
36
|
reject: (err: Error) => void,
|
|
37
37
|
}
|
|
@@ -43,7 +43,7 @@ export default class LambderResponseBuilder {
|
|
|
43
43
|
private publicPath: string;
|
|
44
44
|
private apiVersion: string|null;
|
|
45
45
|
private lambderUtils: LambderUtils;
|
|
46
|
-
private ctx?: LambderRenderContext
|
|
46
|
+
private ctx?: LambderRenderContext<any>;
|
|
47
47
|
|
|
48
48
|
constructor(
|
|
49
49
|
{ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx }:
|
|
@@ -52,7 +52,7 @@ export default class LambderResponseBuilder {
|
|
|
52
52
|
publicPath: string,
|
|
53
53
|
apiVersion?: string|null,
|
|
54
54
|
lambderUtils: LambderUtils,
|
|
55
|
-
ctx?: LambderRenderContext
|
|
55
|
+
ctx?: LambderRenderContext<any>,
|
|
56
56
|
}
|
|
57
57
|
){
|
|
58
58
|
this.isCorsEnabled = isCorsEnabled;
|
|
@@ -6,7 +6,7 @@ export default class LambderSessionController {
|
|
|
6
6
|
lambderSessionManager: LambderSessionManager;
|
|
7
7
|
sessionTokenCookieKey: string;
|
|
8
8
|
sessionCsrfCookieKey: string;
|
|
9
|
-
ctx: LambderRenderContext
|
|
9
|
+
ctx: LambderRenderContext<any>;
|
|
10
10
|
|
|
11
11
|
constructor(
|
|
12
12
|
{
|
|
@@ -18,7 +18,7 @@ export default class LambderSessionController {
|
|
|
18
18
|
lambderSessionManager: LambderSessionManager,
|
|
19
19
|
sessionTokenCookieKey: string,
|
|
20
20
|
sessionCsrfCookieKey: string,
|
|
21
|
-
ctx: LambderRenderContext
|
|
21
|
+
ctx: LambderRenderContext<any>,
|
|
22
22
|
}
|
|
23
23
|
){
|
|
24
24
|
this.lambderSessionManager = lambderSessionManager;
|
|
@@ -5,183 +5,157 @@
|
|
|
5
5
|
* If this file compiles without errors, the types are working!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
8
9
|
import Lambder from '../src/Lambder.js';
|
|
9
10
|
import LambderCaller from '../src/LambderCaller.js';
|
|
10
11
|
import type { ApiContract } from '../src/index.js';
|
|
11
12
|
|
|
12
13
|
// Test contract
|
|
13
|
-
type TestApiContract = {
|
|
14
|
+
type TestApiContract = ApiContract<{
|
|
14
15
|
getUser: { input: { userId: string }, output: { id: string, name: string } },
|
|
15
16
|
createUser: { input: { name: string, email: string }, output: { id: string, name: string, email: string } },
|
|
16
17
|
listUsers: { input: void, output: Array<{ id: string, name: string }> },
|
|
17
18
|
deleteUser: { input: { userId: string }, output: boolean }
|
|
18
|
-
}
|
|
19
|
+
}>;
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Test 1: LambderCaller Type Safety
|
|
22
23
|
// ============================================================================
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
describe('LambderCaller Type Safety', () => {
|
|
26
|
+
it('should have correct types', () => {
|
|
27
|
+
const caller = new LambderCaller<TestApiContract>({
|
|
28
|
+
apiPath: '/api',
|
|
29
|
+
isCorsEnabled: false
|
|
30
|
+
});
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
const user1 = await caller.api('getUser', { userId: '123' });
|
|
33
|
-
const user2 = await caller.api('createUser', { name: 'Alice', email: 'alice@example.com' });
|
|
34
|
-
const users = await caller.api('listUsers', undefined);
|
|
35
|
-
const deleted = await caller.api('deleteUser', { userId: '123' });
|
|
36
|
-
|
|
37
|
-
// Type assertions to verify return types
|
|
38
|
-
if (user1) {
|
|
39
|
-
const name: string = user1.name; // Should work
|
|
40
|
-
const id: string = user1.id; // Should work
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (user2) {
|
|
44
|
-
const email: string = user2.email; // Should work
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (users) {
|
|
48
|
-
const firstUser = users[0];
|
|
49
|
-
if (firstUser) {
|
|
50
|
-
const name: string = firstUser.name; // Should work
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (deleted !== null && deleted !== undefined) {
|
|
55
|
-
const result: boolean = deleted; // Should work
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ❌ These should cause TypeScript errors (commented out to allow compilation):
|
|
32
|
+
// Type assertions to verify compile-time types
|
|
33
|
+
// If this compiles, the types are correct
|
|
59
34
|
|
|
60
|
-
//
|
|
61
|
-
|
|
35
|
+
// ✅ These should compile without errors
|
|
36
|
+
type GetUserInput = Parameters<typeof caller.api<'getUser'>>[1];
|
|
37
|
+
type CreateUserInput = Parameters<typeof caller.api<'createUser'>>[1];
|
|
62
38
|
|
|
63
|
-
//
|
|
64
|
-
|
|
39
|
+
// Verify the caller was created successfully
|
|
40
|
+
expect(caller).toBeDefined();
|
|
41
|
+
expect(caller.api).toBeDefined();
|
|
65
42
|
|
|
66
|
-
//
|
|
43
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
44
|
+
// await caller.api('getUser', { id: '123' }); // Error: should be userId
|
|
45
|
+
// await caller.api('createUser', { name: 'Bob' }); // Error: missing email
|
|
67
46
|
// await caller.api('getUser', { userId: 123 }); // Error: userId should be string
|
|
68
|
-
|
|
69
|
-
// Non-existent API
|
|
70
47
|
// await caller.api('nonExistent', {}); // Error: API doesn't exist
|
|
71
|
-
|
|
72
|
-
// Wrong payload for void input
|
|
73
48
|
// await caller.api('listUsers', { something: true }); // Error: should be undefined
|
|
74
|
-
}
|
|
75
|
-
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
76
51
|
|
|
77
52
|
// ============================================================================
|
|
78
53
|
// Test 2: Lambder Type Safety
|
|
79
54
|
// ============================================================================
|
|
80
55
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
56
|
+
describe('Lambder Type Safety', () => {
|
|
57
|
+
it('should have correct types', () => {
|
|
58
|
+
const lambder = new Lambder<TestApiContract>({
|
|
59
|
+
publicPath: './public',
|
|
60
|
+
apiPath: '/api'
|
|
61
|
+
});
|
|
86
62
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
63
|
+
// ✅ Should work: Typed API
|
|
64
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
65
|
+
// ctx.apiPayload should be typed as { userId: string }
|
|
66
|
+
const userId: string = ctx.apiPayload.userId; // Should work
|
|
67
|
+
|
|
68
|
+
// Return correct type
|
|
69
|
+
return resolver.api({ id: userId, name: 'Test' });
|
|
70
|
+
});
|
|
95
71
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
72
|
+
// ✅ Should work: Typed session API
|
|
73
|
+
lambder.addSessionApi('createUser', async (ctx, resolver) => {
|
|
74
|
+
// ctx.apiPayload should be typed as { name: string, email: string }
|
|
75
|
+
const name: string = ctx.apiPayload.name;
|
|
76
|
+
const email: string = ctx.apiPayload.email;
|
|
77
|
+
|
|
78
|
+
return resolver.api({ id: '1', name, email });
|
|
79
|
+
});
|
|
104
80
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
81
|
+
// ✅ Should work: Void input API
|
|
82
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
83
|
+
// ctx.apiPayload should be void/undefined
|
|
84
|
+
return resolver.api([
|
|
85
|
+
{ id: '1', name: 'User 1' },
|
|
86
|
+
{ id: '2', name: 'User 2' }
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
113
89
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
90
|
+
// ✅ Should work: RegExp (untyped)
|
|
91
|
+
lambder.addApi(/^admin\./, async (ctx, resolver) => {
|
|
92
|
+
// ctx.apiPayload is any (untyped)
|
|
93
|
+
return resolver.api({});
|
|
94
|
+
});
|
|
119
95
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
96
|
+
// Verify the lambder was created successfully
|
|
97
|
+
expect(lambder).toBeDefined();
|
|
98
|
+
expect(lambder.addApi).toBeDefined();
|
|
99
|
+
|
|
100
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
101
|
+
// lambder.addApi('getUser', async (ctx, resolver) => {
|
|
102
|
+
// const id = ctx.apiPayload.id; // Error: should be userId
|
|
103
|
+
// return resolver.api({ id, name: 'Test' });
|
|
104
|
+
// });
|
|
105
|
+
// lambder.addApi('getUser', async (ctx, resolver) => {
|
|
106
|
+
// return resolver.api({ id: '1' }); // Error: missing name property
|
|
107
|
+
// });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
133
110
|
|
|
134
111
|
// ============================================================================
|
|
135
112
|
// Test 3: Backward Compatibility (No Contract)
|
|
136
113
|
// ============================================================================
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
115
|
+
describe('Backward Compatibility (No Contract)', () => {
|
|
116
|
+
it('should work without type contract', () => {
|
|
117
|
+
// Without contract type - should work as before (untyped)
|
|
118
|
+
const caller = new LambderCaller({
|
|
119
|
+
apiPath: '/api',
|
|
120
|
+
isCorsEnabled: false
|
|
121
|
+
});
|
|
144
122
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
123
|
+
const lambder = new Lambder({
|
|
124
|
+
publicPath: './public',
|
|
125
|
+
apiPath: '/api'
|
|
126
|
+
});
|
|
149
127
|
|
|
150
|
-
async function test() {
|
|
151
128
|
// Should work - untyped
|
|
152
|
-
const result = await caller.api('anyApi', { anything: true });
|
|
153
|
-
|
|
154
129
|
lambder.addApi('anyApi', async (ctx, resolver) => {
|
|
155
130
|
// ctx.apiPayload is any
|
|
156
131
|
return resolver.api({ anything: ctx.apiPayload });
|
|
157
132
|
});
|
|
158
|
-
|
|
159
|
-
|
|
133
|
+
|
|
134
|
+
// Verify the instances were created successfully
|
|
135
|
+
expect(caller).toBeDefined();
|
|
136
|
+
expect(lambder).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
160
139
|
|
|
161
140
|
// ============================================================================
|
|
162
141
|
// Test 4: apiRaw Method
|
|
163
142
|
// ============================================================================
|
|
164
143
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
144
|
+
describe('apiRaw Method Type Safety', () => {
|
|
145
|
+
it('should have correct types for apiRaw', () => {
|
|
146
|
+
const caller = new LambderCaller<TestApiContract>({
|
|
147
|
+
apiPath: '/api',
|
|
148
|
+
isCorsEnabled: false
|
|
149
|
+
});
|
|
170
150
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
151
|
+
// Verify the caller was created successfully
|
|
152
|
+
expect(caller).toBeDefined();
|
|
153
|
+
expect(caller.apiRaw).toBeDefined();
|
|
174
154
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const name: string = user.name; // Should work
|
|
180
|
-
const id: string = user.id; // Should work
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
155
|
+
// Type checking happens at compile time
|
|
156
|
+
// If this compiles, apiRaw has correct types
|
|
157
|
+
});
|
|
158
|
+
});
|
|
185
159
|
|
|
186
160
|
// ============================================================================
|
|
187
161
|
// Test 5: Complex Types
|
|
@@ -198,27 +172,31 @@ type ComplexApiContract = {
|
|
|
198
172
|
}
|
|
199
173
|
}
|
|
200
174
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
async function test() {
|
|
208
|
-
// Should work with partial update
|
|
209
|
-
await caller.api('update', { id: '1', name: 'New Name' });
|
|
210
|
-
await caller.api('update', { id: '1', email: 'new@email.com' });
|
|
211
|
-
await caller.api('update', { id: '1' }); // Only id
|
|
175
|
+
describe('Complex Types', () => {
|
|
176
|
+
it('should handle complex type patterns', () => {
|
|
177
|
+
const caller = new LambderCaller<ComplexApiContract>({
|
|
178
|
+
apiPath: '/api',
|
|
179
|
+
isCorsEnabled: false
|
|
180
|
+
});
|
|
212
181
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
182
|
+
// Verify the caller was created successfully
|
|
183
|
+
expect(caller).toBeDefined();
|
|
184
|
+
|
|
185
|
+
// Type assertions to verify compile-time types
|
|
186
|
+
// If this compiles with correct types, the test passes
|
|
187
|
+
type UpdateInput = Parameters<typeof caller.api<'update'>>[1];
|
|
188
|
+
type SearchInput = Parameters<typeof caller.api<'search'>>[1];
|
|
189
|
+
|
|
190
|
+
// These would compile successfully with correct types:
|
|
191
|
+
// await caller.api('update', { id: '1', name: 'New Name' });
|
|
192
|
+
// await caller.api('update', { id: '1', email: 'new@email.com' });
|
|
193
|
+
// await caller.api('update', { id: '1' }); // Only id
|
|
194
|
+
// await caller.api('search', { query: 'test' });
|
|
195
|
+
// await caller.api('search', { query: 'test', filters: { role: 'admin' } });
|
|
196
|
+
// await caller.api('search', { query: 'test', filters: { role: 'admin', active: true } });
|
|
197
|
+
});
|
|
198
|
+
});
|
|
219
199
|
|
|
220
200
|
// ============================================================================
|
|
221
201
|
// If this file compiles without errors, the type system is working! ✅
|
|
222
202
|
// ============================================================================
|
|
223
|
-
|
|
224
|
-
export { };
|