lambder 11.1.1 → 12.0.1
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/CHANGELOG.md +137 -0
- package/README.md +18 -13
- package/dist/api/LambderApiDeclarations.d.ts +194 -0
- package/dist/api/LambderApiDeclarations.js +52 -0
- package/dist/api/LambderApiDefinition.d.ts +1 -1
- package/dist/api/LambderApiGuards.d.ts +21 -22
- package/dist/api/LambderApiGuards.js +1 -1
- package/dist/api/LambderApiRateLimits.d.ts +19 -19
- package/dist/api/LambderApiRateLimits.js +1 -1
- package/dist/api/LambderApiRequest.d.ts +15 -6
- package/dist/api/LambderApiRequest.js +8 -9
- package/dist/build/generatedTables.d.ts +2 -1
- package/dist/build/generatedTables.js +1 -1
- package/dist/client/LambderCaller.d.ts +36 -23
- package/dist/client/LambderCaller.js +88 -67
- package/dist/client/lambderFetchTransport.js +2 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/core/Lambder.d.ts +203 -87
- package/dist/core/Lambder.js +265 -86
- package/dist/core/LambderContext.d.ts +1 -1
- package/dist/core/LambderContext.js +13 -5
- package/dist/core/LambderCreateOptions.d.ts +28 -39
- package/dist/core/LambderCreateOptions.js +2 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +2 -0
- package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
- package/dist/invoke/LambderInvokeCaller.js +110 -94
- package/dist/invoke/LambderLambdaEvent.d.ts +0 -1
- package/dist/invoke/LambderLambdaEvent.js +0 -1
- package/dist/invoke/lambderHandlerTransport.js +2 -1
- package/dist/mock/LambderMockApp.d.ts +31 -25
- package/dist/mock/LambderMockApp.js +52 -45
- package/dist/mock/LambderMockCreateOptions.d.ts +6 -7
- package/dist/mock/LambderMockTypes.d.ts +44 -7
- package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
- package/dist/mock/lambderMockInvokeTransport.js +15 -7
- package/dist/mock/lambderMockMswHandler.js +8 -4
- package/dist/mock.d.ts +2 -2
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/transport/LambderApiTransport.js +2 -3
- package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/shared/wire/LambderApiContract.d.ts +4 -18
- package/dist/shared/wire/LambderApiContract.js +1 -1
- package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
- package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
- package/dist/shared/wire/LambderApiNames.d.ts +55 -0
- package/dist/shared/wire/LambderApiNames.js +81 -0
- package/dist/shared/wire/LambderUploadSchemas.js +2 -1
- package/dist/testing/LambderTestApp.d.ts +5 -1
- package/dist/testing/LambderTestVisitor.d.ts +16 -8
- package/dist/testing/LambderTestVisitor.js +33 -25
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isActionName, isGroupName } from "./LambderApiNames.js";
|
|
2
|
+
/**
|
|
3
|
+
* The caller, answering for each group a contract could name besides its
|
|
4
|
+
* own members. A member the caller has (a method, a field) is always the
|
|
5
|
+
* caller's; a name that could be a group (see isGroupName: an identifier no
|
|
6
|
+
* caller member has) is one; anything else a runtime or a framework asks of
|
|
7
|
+
* an object (then, toJSON, a symbol, `__v_isRef`) is what the caller itself
|
|
8
|
+
* answers, so a caller is never mistaken for a promise, a ref or a
|
|
9
|
+
* serializable record.
|
|
10
|
+
*
|
|
11
|
+
* A caller keeps its state in #private fields, which no property name
|
|
12
|
+
* reaches, so a group may take any name its public members leave free. Such
|
|
13
|
+
* a field is readable only on the caller itself, never through this proxy,
|
|
14
|
+
* so a member is read and a method run on the caller; a method that returns
|
|
15
|
+
* the caller (setTransport) hands back the proxy, groups and all.
|
|
16
|
+
*
|
|
17
|
+
* A group answers the same way one level down: an ordinary object's own
|
|
18
|
+
* members (toString, valueOf, hasOwnProperty) are an ordinary object's, so
|
|
19
|
+
* converting a group to a string or asking it what it holds calls no
|
|
20
|
+
* endpoint; any other name that could be an action (see isActionName) is
|
|
21
|
+
* one. A group is read-only, like the type that describes it.
|
|
22
|
+
*/
|
|
23
|
+
export const withApiGroupCalls = (caller, call, outcome) => {
|
|
24
|
+
const methods = new WeakMap();
|
|
25
|
+
const methodOnCaller = (method) => {
|
|
26
|
+
let bound = methods.get(method);
|
|
27
|
+
if (!bound) {
|
|
28
|
+
bound = (...args) => {
|
|
29
|
+
const result = Reflect.apply(method, caller, args);
|
|
30
|
+
return result === caller ? withGroups : result;
|
|
31
|
+
};
|
|
32
|
+
methods.set(method, bound);
|
|
33
|
+
}
|
|
34
|
+
return bound;
|
|
35
|
+
};
|
|
36
|
+
const groups = new Map();
|
|
37
|
+
const groupOf = (group) => {
|
|
38
|
+
let calls = groups.get(group);
|
|
39
|
+
if (!calls) {
|
|
40
|
+
const actions = new Map();
|
|
41
|
+
calls = new Proxy(Object.freeze({}), {
|
|
42
|
+
get: (target, action) => {
|
|
43
|
+
if (typeof action !== "string" || action in target || !isActionName(action))
|
|
44
|
+
return Reflect.get(target, action);
|
|
45
|
+
let endpoint = actions.get(action);
|
|
46
|
+
if (!endpoint) {
|
|
47
|
+
const apiName = `${group}.${action}`;
|
|
48
|
+
endpoint = Object.freeze(Object.assign((...args) => call(apiName, args), { outcome: (...args) => outcome(apiName, args) }));
|
|
49
|
+
actions.set(action, endpoint);
|
|
50
|
+
}
|
|
51
|
+
return endpoint;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
groups.set(group, calls);
|
|
55
|
+
}
|
|
56
|
+
return calls;
|
|
57
|
+
};
|
|
58
|
+
const withGroups = new Proxy(caller, {
|
|
59
|
+
get: (target, property) => {
|
|
60
|
+
if (typeof property === "string" && !(property in target) && isGroupName(property))
|
|
61
|
+
return groupOf(property);
|
|
62
|
+
const value = Reflect.get(target, property, target);
|
|
63
|
+
return typeof value === "function" && property !== "constructor" ? methodOnCaller(value) : value;
|
|
64
|
+
},
|
|
65
|
+
set: (target, property, value) => Reflect.set(target, property, value, target),
|
|
66
|
+
});
|
|
67
|
+
return withGroups;
|
|
68
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an endpoint is named and where it is called. An endpoint belongs to a
|
|
3
|
+
* group, and its name is the two joined by a dot: `orders.place`. Both are
|
|
4
|
+
* identifiers, so the name is also a path: a call posts to
|
|
5
|
+
* `{apiPath}/{group}/{action}`, which a gateway, a CDN and a log can meter,
|
|
6
|
+
* limit and read per endpoint without opening the body.
|
|
7
|
+
*
|
|
8
|
+
* The group is also a property of every Lambder caller
|
|
9
|
+
* (`caller.orders.place(input)`), so a group may not be named like anything a
|
|
10
|
+
* caller already has.
|
|
11
|
+
*/
|
|
12
|
+
/** A group's or an action's name: a letter, then letters, digits or underscores. */
|
|
13
|
+
export declare const LAMBDER_API_NAME_SEGMENT: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* Every public member of the three callers a group becomes a property of
|
|
16
|
+
* (LambderCaller, LambderInvokeCaller, LambderTestVisitor), since a caller
|
|
17
|
+
* looks a group up only after its own members. Their state is #private,
|
|
18
|
+
* which no property name reaches, so only these can stand in a group's way.
|
|
19
|
+
* A test holds this to the callers' public members, in their types and at
|
|
20
|
+
* runtime, both ways.
|
|
21
|
+
*/
|
|
22
|
+
export declare const LAMBDER_CALLER_MEMBER_NAMES: readonly ["api", "apiOutcome", "caller", "clientIp", "fetchTrackerList", "host", "isLoading", "jar", "request", "setSessionCookieKey", "setTransport", "signIn"];
|
|
23
|
+
/**
|
|
24
|
+
* Names no group may take: the callers' members, and what a JavaScript
|
|
25
|
+
* runtime, a promise check, a serializer or a framework asks of any object.
|
|
26
|
+
*/
|
|
27
|
+
export declare const LAMBDER_RESERVED_GROUP_NAMES: readonly ["api", "apiOutcome", "caller", "clientIp", "fetchTrackerList", "host", "isLoading", "jar", "request", "setSessionCookieKey", "setTransport", "signIn", "constructor", "then", "catch", "finally", "toJSON", "toString", "valueOf", "toLocaleString", "prototype", "inspect", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable"];
|
|
28
|
+
/** A name no group may take; see LAMBDER_RESERVED_GROUP_NAMES. */
|
|
29
|
+
export type LambderReservedGroupName = typeof LAMBDER_RESERVED_GROUP_NAMES[number];
|
|
30
|
+
/**
|
|
31
|
+
* Names no action may take: what a runtime, a promise check or a serializer
|
|
32
|
+
* asks of a group or of the function a caller hands out for an action, and
|
|
33
|
+
* the members every object has (toString, valueOf, hasOwnProperty), which a
|
|
34
|
+
* group keeps as its own.
|
|
35
|
+
*/
|
|
36
|
+
export declare const LAMBDER_RESERVED_ACTION_NAMES: readonly ["then", "catch", "finally", "toJSON", "constructor", "prototype", "outcome", "bind", "call", "apply", "length", "name", "arguments", "caller", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable"];
|
|
37
|
+
/** A name no action may take; see LAMBDER_RESERVED_ACTION_NAMES. */
|
|
38
|
+
export type LambderReservedActionName = typeof LAMBDER_RESERVED_ACTION_NAMES[number];
|
|
39
|
+
/** Whether a name can be a group: an identifier segment, and not one a caller already answers for. */
|
|
40
|
+
export declare const isGroupName: (name: string) => boolean;
|
|
41
|
+
/** Whether a name can be an action of a group: an identifier segment, and not one a function already answers for. */
|
|
42
|
+
export declare const isActionName: (name: string) => boolean;
|
|
43
|
+
/** An endpoint's name split into its group and action, or null for a name that is not two segments. */
|
|
44
|
+
export declare const splitApiName: (apiName: string) => {
|
|
45
|
+
group: string;
|
|
46
|
+
action: string;
|
|
47
|
+
} | null;
|
|
48
|
+
/** Where a call to the endpoint goes: `{apiPath}/{group}/{action}`. An apiPath may be absolute (another origin) or end with a slash. */
|
|
49
|
+
export declare const apiCallPath: (apiPath: string, apiName: string) => string;
|
|
50
|
+
/**
|
|
51
|
+
* The endpoint a request path calls, or null when the path is not a call
|
|
52
|
+
* under apiPath: exactly `{apiPath}/{group}/{action}`, with no query, no
|
|
53
|
+
* trailing slash and no further segment.
|
|
54
|
+
*/
|
|
55
|
+
export declare const apiNameOfCallPath: (apiPath: string, path: string) => string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an endpoint is named and where it is called. An endpoint belongs to a
|
|
3
|
+
* group, and its name is the two joined by a dot: `orders.place`. Both are
|
|
4
|
+
* identifiers, so the name is also a path: a call posts to
|
|
5
|
+
* `{apiPath}/{group}/{action}`, which a gateway, a CDN and a log can meter,
|
|
6
|
+
* limit and read per endpoint without opening the body.
|
|
7
|
+
*
|
|
8
|
+
* The group is also a property of every Lambder caller
|
|
9
|
+
* (`caller.orders.place(input)`), so a group may not be named like anything a
|
|
10
|
+
* caller already has.
|
|
11
|
+
*/
|
|
12
|
+
/** A group's or an action's name: a letter, then letters, digits or underscores. */
|
|
13
|
+
export const LAMBDER_API_NAME_SEGMENT = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
14
|
+
/**
|
|
15
|
+
* Every public member of the three callers a group becomes a property of
|
|
16
|
+
* (LambderCaller, LambderInvokeCaller, LambderTestVisitor), since a caller
|
|
17
|
+
* looks a group up only after its own members. Their state is #private,
|
|
18
|
+
* which no property name reaches, so only these can stand in a group's way.
|
|
19
|
+
* A test holds this to the callers' public members, in their types and at
|
|
20
|
+
* runtime, both ways.
|
|
21
|
+
*/
|
|
22
|
+
export const LAMBDER_CALLER_MEMBER_NAMES = [
|
|
23
|
+
"api", "apiOutcome", "caller", "clientIp", "fetchTrackerList", "host", "isLoading", "jar", "request",
|
|
24
|
+
"setSessionCookieKey", "setTransport", "signIn",
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Names no group may take: the callers' members, and what a JavaScript
|
|
28
|
+
* runtime, a promise check, a serializer or a framework asks of any object.
|
|
29
|
+
*/
|
|
30
|
+
export const LAMBDER_RESERVED_GROUP_NAMES = [
|
|
31
|
+
...LAMBDER_CALLER_MEMBER_NAMES,
|
|
32
|
+
"constructor", "then", "catch", "finally", "toJSON", "toString", "valueOf", "toLocaleString", "prototype", "inspect",
|
|
33
|
+
"hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable",
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Names no action may take: what a runtime, a promise check or a serializer
|
|
37
|
+
* asks of a group or of the function a caller hands out for an action, and
|
|
38
|
+
* the members every object has (toString, valueOf, hasOwnProperty), which a
|
|
39
|
+
* group keeps as its own.
|
|
40
|
+
*/
|
|
41
|
+
export const LAMBDER_RESERVED_ACTION_NAMES = [
|
|
42
|
+
"then", "catch", "finally", "toJSON", "constructor", "prototype", "outcome", "bind", "call", "apply", "length", "name",
|
|
43
|
+
"arguments", "caller", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable",
|
|
44
|
+
];
|
|
45
|
+
const RESERVED_GROUP_NAMES = new Set(LAMBDER_RESERVED_GROUP_NAMES);
|
|
46
|
+
const RESERVED_ACTION_NAMES = new Set(LAMBDER_RESERVED_ACTION_NAMES);
|
|
47
|
+
/** Whether a name can be a group: an identifier segment, and not one a caller already answers for. */
|
|
48
|
+
export const isGroupName = (name) => LAMBDER_API_NAME_SEGMENT.test(name) && !RESERVED_GROUP_NAMES.has(name);
|
|
49
|
+
/** Whether a name can be an action of a group: an identifier segment, and not one a function already answers for. */
|
|
50
|
+
export const isActionName = (name) => LAMBDER_API_NAME_SEGMENT.test(name) && !RESERVED_ACTION_NAMES.has(name);
|
|
51
|
+
/** An endpoint's name split into its group and action, or null for a name that is not two segments. */
|
|
52
|
+
export const splitApiName = (apiName) => {
|
|
53
|
+
const dot = apiName.indexOf(".");
|
|
54
|
+
if (dot === -1)
|
|
55
|
+
return null;
|
|
56
|
+
const group = apiName.slice(0, dot);
|
|
57
|
+
const action = apiName.slice(dot + 1);
|
|
58
|
+
return LAMBDER_API_NAME_SEGMENT.test(group) && LAMBDER_API_NAME_SEGMENT.test(action) ? { group, action } : null;
|
|
59
|
+
};
|
|
60
|
+
/** Where a call to the endpoint goes: `{apiPath}/{group}/{action}`. An apiPath may be absolute (another origin) or end with a slash. */
|
|
61
|
+
export const apiCallPath = (apiPath, apiName) => {
|
|
62
|
+
const parts = splitApiName(apiName);
|
|
63
|
+
if (!parts)
|
|
64
|
+
throw new Error(`Lambder: "${apiName}" is not an endpoint name. An endpoint is named group.action, both identifiers.`);
|
|
65
|
+
return `${apiPath.replace(/\/+$/, "")}/${parts.group}/${parts.action}`;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The endpoint a request path calls, or null when the path is not a call
|
|
69
|
+
* under apiPath: exactly `{apiPath}/{group}/{action}`, with no query, no
|
|
70
|
+
* trailing slash and no further segment.
|
|
71
|
+
*/
|
|
72
|
+
export const apiNameOfCallPath = (apiPath, path) => {
|
|
73
|
+
const base = apiPath.replace(/\/+$/, "");
|
|
74
|
+
if (!path.startsWith(`${base}/`))
|
|
75
|
+
return null;
|
|
76
|
+
const segments = path.slice(base.length + 1).split("/");
|
|
77
|
+
if (segments.length !== 2)
|
|
78
|
+
return null;
|
|
79
|
+
const [group, action] = segments;
|
|
80
|
+
return LAMBDER_API_NAME_SEGMENT.test(group) && LAMBDER_API_NAME_SEGMENT.test(action) ? `${group}.${action}` : null;
|
|
81
|
+
};
|
|
@@ -5,9 +5,10 @@ import { z } from "zod";
|
|
|
5
5
|
* posts to the ticket endpoint, and the ticket it answers.
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
|
-
*
|
|
8
|
+
* requestUpload: defineApi({
|
|
9
9
|
* input: z.object({ folderId: z.uuid(), fileFacts: LambderUploadFileFactsSchema }),
|
|
10
10
|
* output: z.object({ ticket: LambderUploadTicketSchema, documentId: z.uuid() }),
|
|
11
|
+
* guards: "signedIn",
|
|
11
12
|
* }, ...)
|
|
12
13
|
* ```
|
|
13
14
|
*
|
|
@@ -7,6 +7,7 @@ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js
|
|
|
7
7
|
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
8
8
|
import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
|
|
9
9
|
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
10
|
+
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
10
11
|
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
11
12
|
import { LambderTestVisitor, type LambderTestVisitorArgs, type LambderTestVisitorOptions } from "./LambderTestVisitor.js";
|
|
12
13
|
/**
|
|
@@ -51,7 +52,10 @@ export type LambderTestAppOptions = {
|
|
|
51
52
|
* (`lambderTestApp<SessionData, ApiContractType>(lambder)`), so the tests type
|
|
52
53
|
* their calls against plain members rather than the chained intersection.
|
|
53
54
|
*/
|
|
54
|
-
export type LambderTestedInstance<TSessionData, TContract> = Lambder<
|
|
55
|
+
export type LambderTestedInstance<TSessionData, TContract> = Pick<Lambder<any, any>, "apiPath" | "getHandler" | "getSessionController" | "getSessionManager" | typeof LAMBDER_BACKEND_SWAP | typeof LAMBDER_CRASH_WATCH> & {
|
|
56
|
+
readonly AppTypes: {
|
|
57
|
+
session: TSessionData;
|
|
58
|
+
};
|
|
55
59
|
readonly ApiContract: TContract;
|
|
56
60
|
};
|
|
57
61
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import LambderCaller from "../client/LambderCaller.js";
|
|
1
|
+
import LambderCaller, { type LambderCallerGroupCalls, type LambderCallerMembers } from "../client/LambderCaller.js";
|
|
2
2
|
import type { LambderHttpEventFormat } from "../core/LambderContext.js";
|
|
3
3
|
import type { LambderHandler } from "../core/LambderCreateOptions.js";
|
|
4
4
|
import { type LambderLambdaHttpResult } from "../invoke/LambderLambdaEvent.js";
|
|
@@ -84,7 +84,8 @@ export type LambderTestVisitorArgs<TOptions, TProvidedGuards extends string> = [
|
|
|
84
84
|
*
|
|
85
85
|
* Created by `LambderTestApp.visitor()` and `signIn()`, not constructed.
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
declare class LambderTestVisitorCore<TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never> {
|
|
88
|
+
#private;
|
|
88
89
|
readonly host: string;
|
|
89
90
|
readonly clientIp: string;
|
|
90
91
|
/**
|
|
@@ -95,7 +96,7 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
95
96
|
*/
|
|
96
97
|
readonly caller: LambderCaller<TContract, TProvidedGuards>;
|
|
97
98
|
/** The payload on success, `undefined` on a failure: LambderCaller.api, through this visitor. */
|
|
98
|
-
readonly api:
|
|
99
|
+
readonly api: LambderCallerMembers<TContract, TProvidedGuards>["api"];
|
|
99
100
|
/**
|
|
100
101
|
* The full outcome, never throwing: LambderCaller.apiOutcome, through
|
|
101
102
|
* this visitor. Pair it with assertApiSuccess / assertApiFailure.
|
|
@@ -105,11 +106,7 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
105
106
|
* 500"; here that error's `cause` is what the app actually threw, stack
|
|
106
107
|
* included, so a failing test points at the line in the handler.
|
|
107
108
|
*/
|
|
108
|
-
readonly apiOutcome:
|
|
109
|
-
private readonly wiring;
|
|
110
|
-
private readonly headers;
|
|
111
|
-
private readonly cookieJar;
|
|
112
|
-
private seenResetCount;
|
|
109
|
+
readonly apiOutcome: LambderCallerMembers<TContract, TProvidedGuards>["apiOutcome"];
|
|
113
110
|
constructor(wiring: LambderTestVisitorWiring<TSessionData>, options: LambderTestVisitorOptions<TContract, TProvidedGuards> & {
|
|
114
111
|
host: string;
|
|
115
112
|
clientIp: string;
|
|
@@ -151,3 +148,14 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
151
148
|
ttlSeconds?: number;
|
|
152
149
|
}): Promise<LambderCreatedSession<TSessionData>>;
|
|
153
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* One simulated browser in front of a real Lambder app, with the app's
|
|
153
|
+
* endpoints by group as a caller has them: `visitor.orders.place(input)`,
|
|
154
|
+
* `visitor.orders.place.outcome(input)`.
|
|
155
|
+
*/
|
|
156
|
+
export type LambderTestVisitor<TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never> = LambderTestVisitorCore<TContract, TSessionData, TProvidedGuards> & LambderCallerGroupCalls<TContract, TProvidedGuards>;
|
|
157
|
+
export declare const LambderTestVisitor: {
|
|
158
|
+
new <TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never>(...args: ConstructorParameters<typeof LambderTestVisitorCore<TContract, TSessionData, TProvidedGuards>>): LambderTestVisitor<TContract, TSessionData, TProvidedGuards>;
|
|
159
|
+
readonly prototype: LambderTestVisitorCore<any, any, any>;
|
|
160
|
+
};
|
|
161
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import LambderCaller from "../client/LambderCaller.js";
|
|
2
|
+
import { withApiGroupCalls } from "../shared/wire/LambderApiGroupCalls.js";
|
|
2
3
|
import { lambderHandlerTransport } from "../invoke/lambderHandlerTransport.js";
|
|
3
4
|
import { decodeLambdaHttpResult, localLambdaContext, synthesizeLambdaHttpEvent, } from "../invoke/LambderLambdaEvent.js";
|
|
4
5
|
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
@@ -15,7 +16,7 @@ import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderReques
|
|
|
15
16
|
*
|
|
16
17
|
* Created by `LambderTestApp.visitor()` and `signIn()`, not constructed.
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
+
class LambderTestVisitorCore {
|
|
19
20
|
host;
|
|
20
21
|
clientIp;
|
|
21
22
|
/**
|
|
@@ -37,19 +38,19 @@ export class LambderTestVisitor {
|
|
|
37
38
|
* included, so a failing test points at the line in the handler.
|
|
38
39
|
*/
|
|
39
40
|
apiOutcome;
|
|
40
|
-
wiring;
|
|
41
|
-
headers;
|
|
42
|
-
cookieJar;
|
|
43
|
-
seenResetCount;
|
|
41
|
+
#wiring;
|
|
42
|
+
#headers;
|
|
43
|
+
#cookieJar;
|
|
44
|
+
#seenResetCount;
|
|
44
45
|
constructor(wiring, options) {
|
|
45
|
-
this
|
|
46
|
+
this.#wiring = wiring;
|
|
46
47
|
this.host = options.host;
|
|
47
48
|
this.clientIp = options.clientIp;
|
|
48
|
-
this
|
|
49
|
-
this
|
|
50
|
-
this
|
|
49
|
+
this.#headers = options.headers ?? {};
|
|
50
|
+
this.#cookieJar = new LambderCookieJar({ host: this.host });
|
|
51
|
+
this.#seenResetCount = wiring.resetCount();
|
|
51
52
|
const handlerTransport = lambderHandlerTransport(wiring.handler, { host: this.host, clientIp: this.clientIp, eventFormat: wiring.eventFormat });
|
|
52
|
-
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this
|
|
53
|
+
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this.#headers, ...request.headers } });
|
|
53
54
|
this.caller = new LambderCaller({
|
|
54
55
|
apiPath: wiring.apiPath,
|
|
55
56
|
apiVersion: options.apiVersion,
|
|
@@ -69,13 +70,19 @@ export class LambderTestVisitor {
|
|
|
69
70
|
if (wiring.sessionCookieNames) {
|
|
70
71
|
this.caller.setSessionCookieKey(wiring.sessionCookieNames.tokenCookieKey, wiring.sessionCookieNames.csrfCookieKey);
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const { result: outcome, crash } = await wiring.watchCrash(() =>
|
|
73
|
+
const caller = this.caller;
|
|
74
|
+
const apiOutcome = async (apiName, ...callArgs) => {
|
|
75
|
+
const { result: outcome, crash } = await wiring.watchCrash(() => caller.apiOutcome(apiName, ...callArgs));
|
|
75
76
|
if (crash && !outcome.ok && "error" in outcome && outcome.error.cause === undefined)
|
|
76
77
|
outcome.error.cause = crash;
|
|
77
78
|
return outcome;
|
|
78
|
-
}
|
|
79
|
+
};
|
|
80
|
+
this.api = ((apiName, ...callArgs) => caller.api(apiName, ...callArgs));
|
|
81
|
+
this.apiOutcome = apiOutcome;
|
|
82
|
+
// Each group of the contract, as on a caller, through this visitor's
|
|
83
|
+
// own two calls: visitor.orders.place.outcome(input) carries the crash
|
|
84
|
+
// cause as visitor.apiOutcome does.
|
|
85
|
+
return withApiGroupCalls(this, (apiName, args) => caller.api(apiName, ...args), (apiName, args) => apiOutcome(apiName, ...args));
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* This visitor's cookies, to inspect or clear; every call and request
|
|
@@ -84,12 +91,12 @@ export class LambderTestVisitor {
|
|
|
84
91
|
* emptied store would read as signed in until an answer said otherwise.
|
|
85
92
|
*/
|
|
86
93
|
get jar() {
|
|
87
|
-
const resetCount = this
|
|
88
|
-
if (resetCount !== this
|
|
89
|
-
this
|
|
90
|
-
this
|
|
94
|
+
const resetCount = this.#wiring.resetCount();
|
|
95
|
+
if (resetCount !== this.#seenResetCount) {
|
|
96
|
+
this.#cookieJar.clear();
|
|
97
|
+
this.#seenResetCount = resetCount;
|
|
91
98
|
}
|
|
92
|
-
return this
|
|
99
|
+
return this.#cookieJar;
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* One HTTP request to the app that is not an API call, answered by
|
|
@@ -117,12 +124,12 @@ export class LambderTestVisitor {
|
|
|
117
124
|
path: pathname,
|
|
118
125
|
query,
|
|
119
126
|
host: this.host,
|
|
120
|
-
headers: { ...this
|
|
127
|
+
headers: { ...this.#headers, ...init.headers },
|
|
121
128
|
clientIp: this.clientIp,
|
|
122
129
|
cookies: this.jar.cookiePairs(cookieScope),
|
|
123
130
|
body: init.body,
|
|
124
|
-
}, { invoke: false, eventFormat: this
|
|
125
|
-
const result = await decodeLambdaHttpResult(await this
|
|
131
|
+
}, { invoke: false, eventFormat: this.#wiring.eventFormat });
|
|
132
|
+
const result = await decodeLambdaHttpResult(await this.#wiring.handler(event, localLambdaContext("lambder-test")), DEFAULT_MAX_RESTORED_PAYLOAD_BYTES);
|
|
126
133
|
if (result.cookies.length)
|
|
127
134
|
this.jar.storeSetCookies(result.cookies, cookieScope);
|
|
128
135
|
return result;
|
|
@@ -140,11 +147,11 @@ export class LambderTestVisitor {
|
|
|
140
147
|
* answer sessionExpired with nothing to say why.
|
|
141
148
|
*/
|
|
142
149
|
async signIn(sessionKey, data, options = {}) {
|
|
143
|
-
if (!this
|
|
150
|
+
if (!this.#wiring.sessionCookieNames)
|
|
144
151
|
throw new Error("LambderTestVisitor: signIn() needs an app with sessions. Pass the session option to create().");
|
|
145
|
-
const { created, setCookies } = await this
|
|
152
|
+
const { created, setCookies } = await this.#wiring.issueSession(this.host, sessionKey, data, options.ttlSeconds);
|
|
146
153
|
this.jar.storeSetCookies(setCookies, { host: this.host });
|
|
147
|
-
if (this.jar.get(this
|
|
154
|
+
if (this.jar.get(this.#wiring.sessionCookieNames.tokenCookieKey, { host: this.host, includeHttpOnly: true }) === undefined) {
|
|
148
155
|
throw new Error(`LambderTestVisitor: the session cookie did not stick for host "${this.host}", the way a browser on that host would drop it. ` +
|
|
149
156
|
"The app most likely scopes its session cookie to a domain this host is not under: " +
|
|
150
157
|
"give the test app or this visitor a `host` the cookie domain covers.");
|
|
@@ -152,3 +159,4 @@ export class LambderTestVisitor {
|
|
|
152
159
|
return created;
|
|
153
160
|
}
|
|
154
161
|
}
|
|
162
|
+
export const LambderTestVisitor = LambderTestVisitorCore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|