lambder 6.0.2 → 7.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of the mock runtime: what a registry entry is, what a handler
|
|
3
|
+
* receives, how a registry is checked against a contract, and what the
|
|
4
|
+
* subscription emits. Everything a consumer needs at runtime it restates in
|
|
5
|
+
* the builders; these types pin every restatement to the contract type, so
|
|
6
|
+
* the contract stays a type-only import in the consuming app.
|
|
7
|
+
*/
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
10
|
+
import type { LambderApiGuard, LambderGuardDataOf, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
|
|
11
|
+
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
12
|
+
import type { LambderApiCallContext } from "../api/LambderApiCallContext.js";
|
|
13
|
+
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
14
|
+
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
15
|
+
import type { LambderApiDefinition } from "../api/LambderApiDefinition.js";
|
|
16
|
+
import type { LambderApiEnvelopeBody } from "../shared/wire/LambderApiContract.js";
|
|
17
|
+
import type { LambderAppRefusalMessage } from "../shared/wire/LambderApiRefusal.js";
|
|
18
|
+
import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
|
|
19
|
+
import type LambderSessionController from "../session/LambderSessionController.js";
|
|
20
|
+
export type LambderMockInputOf<C, K extends keyof C> = C[K] extends {
|
|
21
|
+
input: infer I;
|
|
22
|
+
} ? I : never;
|
|
23
|
+
export type LambderMockOutputOf<C, K extends keyof C> = C[K] extends {
|
|
24
|
+
output: infer O;
|
|
25
|
+
} ? O : never;
|
|
26
|
+
/**
|
|
27
|
+
* Maps every key an option object carries that its shape does not have to
|
|
28
|
+
* `never`, so a typo is an error on the key itself.
|
|
29
|
+
*
|
|
30
|
+
* Needed once per nesting level: inferring a generic from an object literal
|
|
31
|
+
* (`const G`, `const P` on create()) switches excess-property checking off for
|
|
32
|
+
* the WHOLE literal, nested objects included, so `idempotency: { failOpn:
|
|
33
|
+
* false }` compiled, was dropped in silence, and left the runtime on the
|
|
34
|
+
* default. Intersected into a nested position rather than applied to the
|
|
35
|
+
* option type as a whole, because the type variable has to stay naked
|
|
36
|
+
* somewhere for the literal to be inferred from at all.
|
|
37
|
+
*
|
|
38
|
+
* `unknown` for anything that is not an object (`idempotency: true`), which
|
|
39
|
+
* intersects away: `keyof boolean` is Boolean's own prototype members, and
|
|
40
|
+
* mapping those to `never` would refuse the boolean form outright.
|
|
41
|
+
*
|
|
42
|
+
* The server's create() has the same construction in `LambderNoExtraKeys`
|
|
43
|
+
* (core/Lambder.ts). It is written twice on purpose: `lambder/mock` is
|
|
44
|
+
* browser-safe by its import graph, and reaching into core/ for a type alias
|
|
45
|
+
* would pull the server's whole type graph (aws-lambda included) back into it.
|
|
46
|
+
*/
|
|
47
|
+
export type LambderMockSurplusKeys<TOptions, TShape> = [
|
|
48
|
+
TOptions
|
|
49
|
+
] extends [object] ? Record<Exclude<keyof TOptions, keyof TShape>, never> : unknown;
|
|
50
|
+
/**
|
|
51
|
+
* The context every mock guard and mock handler sees beside its own typed
|
|
52
|
+
* fields: the API core's call context plus the request, a session
|
|
53
|
+
* controller for the call, and the caller's abort signal.
|
|
54
|
+
*/
|
|
55
|
+
export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
|
|
56
|
+
apiName: string;
|
|
57
|
+
request: LambderApiRequest;
|
|
58
|
+
/** Create, rotate, refresh and end sessions, exactly as a server handler does through getSessionController(ctx). */
|
|
59
|
+
sessions: LambderSessionController<S>;
|
|
60
|
+
signal: AbortSignal;
|
|
61
|
+
/**
|
|
62
|
+
* The envelope fields that travel beside the payload, the mock's stand-in
|
|
63
|
+
* for the server's `res.api(payload, config)`: a mock handler returns its
|
|
64
|
+
* payload, so this is where the rest of the envelope goes. `logList` is
|
|
65
|
+
* the usual channel and lives on the context itself.
|
|
66
|
+
*/
|
|
67
|
+
envelope: {
|
|
68
|
+
message?: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/** The same, with the session present: what a `session: true` mock guard and a session endpoint's handler see. */
|
|
72
|
+
export type LambderMockSessionCallContext<S = any> = Omit<LambderMockCallContext<S>, "session"> & {
|
|
73
|
+
session: LambderSessionRecord<S>;
|
|
74
|
+
};
|
|
75
|
+
/** What a mock handler for endpoint K receives. */
|
|
76
|
+
export type LambderMockContext<C, K extends keyof C, S, G> = Omit<LambderMockCallContext<S>, "apiName" | "session" | "guardData"> & {
|
|
77
|
+
apiName: K;
|
|
78
|
+
/** The payload as posted; never optional, the way a validated payload reaches a server handler. */
|
|
79
|
+
payload: LambderMockInputOf<C, K>;
|
|
80
|
+
/** The session on a session endpoint, null on a public one. */
|
|
81
|
+
session: LambderContractMode<C, K> extends "session" ? LambderSessionRecord<S> : LambderContractMode<C, K> extends "public" ? null : LambderSessionRecord<S> | null;
|
|
82
|
+
/** What the endpoint's guards returned, keyed by name, typed from the mock guard map and the endpoint's declaration. */
|
|
83
|
+
guardData: LambderGuardDataOf<LambderGuardMetaMap<G>, LambderContractGuardsOf<C, K>>;
|
|
84
|
+
/** The guard inputs the caller sent, typed by the contract; undefined for an endpoint that declares none. */
|
|
85
|
+
guardInputs: [LambderContractGuardInputsOf<C, K>] extends [never] ? undefined : LambderContractGuardInputsOf<C, K>;
|
|
86
|
+
/** The key the caller sent, when it sent a string; anything else is not a key and reaches a handler as undefined, having already been refused wherever it mattered. */
|
|
87
|
+
idempotencyKey: string | undefined;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The guard map a mock app must declare: one guard per name any endpoint of
|
|
91
|
+
* the contract declares, and for every guard the contract knows in
|
|
92
|
+
* guardInput mode, a `guardInput` schema whose output is what the server
|
|
93
|
+
* inferred. A missing name or a schema that parses to something else fails
|
|
94
|
+
* at the `guards` option.
|
|
95
|
+
*/
|
|
96
|
+
export type LambderMockGuards<C, S = any> = {
|
|
97
|
+
[N in LambderContractGuardNames<C>]: LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
|
|
98
|
+
} & {
|
|
99
|
+
[N in LambderContractGuardInputNames<C>]: {
|
|
100
|
+
guardInput: z.ZodType<LambderContractGuardInput<C, N>, any>;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
export type LambderMockHandler<C, K extends keyof C, S, G> = (ctx: LambderMockContext<C, K, S, G>) => LambderMockOutputOf<C, K> | Promise<LambderMockOutputOf<C, K>>;
|
|
104
|
+
/**
|
|
105
|
+
* The guards field of an entry: required whenever the contract declares any
|
|
106
|
+
* guard for the endpoint, and type-equal to the server's own declaration.
|
|
107
|
+
*
|
|
108
|
+
* Keyed on the guards themselves rather than on guardInputs, because the
|
|
109
|
+
* restatement is the only thing that tells the runtime which guards to run:
|
|
110
|
+
* a guard the entry leaves out simply does not run, and the mock answers 200
|
|
111
|
+
* where the server answers notAuthorized. The guards that carry no
|
|
112
|
+
* guardInput are exactly the "may this role call it" ones (session-only,
|
|
113
|
+
* param-only), so keying on guardInputs made the authorization checks the
|
|
114
|
+
* droppable half.
|
|
115
|
+
*/
|
|
116
|
+
type LambderMockGuardsField<C, K extends keyof C> = [
|
|
117
|
+
LambderContractGuardsOf<C, K>
|
|
118
|
+
] extends [never] ? {
|
|
119
|
+
guards?: never;
|
|
120
|
+
} : {
|
|
121
|
+
guards: LambderContractGuardsOf<C, K>;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The rate-limit field of an entry: required whenever the contract declares
|
|
125
|
+
* one, absent otherwise. Same reasoning as LambderMockGuardsField, and the
|
|
126
|
+
* argument transfers word for word: the restatement is the only thing that
|
|
127
|
+
* tells the runtime to apply the limit, so an entry that leaves it out
|
|
128
|
+
* answers 200 where the server answers 429.
|
|
129
|
+
*/
|
|
130
|
+
type LambderMockRateLimitField<C, K extends keyof C> = [
|
|
131
|
+
LambderContractRateLimitOf<C, K>
|
|
132
|
+
] extends [never] ? {
|
|
133
|
+
rateLimit?: never;
|
|
134
|
+
} : {
|
|
135
|
+
rateLimit: LambderContractRateLimitOf<C, K>;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* The idempotency field of an entry: required whenever the contract declares
|
|
139
|
+
* one. An entry that leaves it out takes no claim and stores no record, so a
|
|
140
|
+
* retry re-runs the handler and the mock answers 200 where the server answers
|
|
141
|
+
* a replay or a 409.
|
|
142
|
+
*/
|
|
143
|
+
type LambderMockIdempotencyField<C, K extends keyof C> = [
|
|
144
|
+
LambderContractIdempotencyOf<C, K>
|
|
145
|
+
] extends [never] ? {
|
|
146
|
+
idempotency?: never;
|
|
147
|
+
} : {
|
|
148
|
+
idempotency: LambderContractIdempotencyOf<C, K>;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* What override() hands back: call restore() to put the original handler
|
|
152
|
+
* back.
|
|
153
|
+
*
|
|
154
|
+
* Restore and nothing else. The handle also carried a `[Symbol.dispose]`
|
|
155
|
+
* member, for `using`, and that member is declared in `lib: ESNext` alone: a
|
|
156
|
+
* consumer on `lib: ES2022` (a Vue app's own setting, and the only consumer
|
|
157
|
+
* this package has) got TS2550 "Property 'dispose' does not exist on type
|
|
158
|
+
* 'SymbolConstructor'" out of the published .d.ts, from importing the entry at
|
|
159
|
+
* all, whenever skipLibCheck was off. A scoped override is a try/finally,
|
|
160
|
+
* which needs no lib.
|
|
161
|
+
*/
|
|
162
|
+
export type LambderMockOverride = {
|
|
163
|
+
restore(): void;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* What an entry's `input` schema must parse to: the endpoint's contract
|
|
167
|
+
* input, in both directions.
|
|
168
|
+
*
|
|
169
|
+
* One direction is not enough, and the missing one is the direction the drift
|
|
170
|
+
* actually travels in. `z.ZodType<Input>` is covariant in its output, so a
|
|
171
|
+
* schema parsing to a SUBTYPE of the contract input passed: an extra required
|
|
172
|
+
* field, or a literal where the contract says string. Such a schema refuses
|
|
173
|
+
* payloads the server accepts, and the mock then answers 422 to a call that
|
|
174
|
+
* works against the real backend, which is the exact failure the schema was
|
|
175
|
+
* added to reproduce, produced by the thing added to reproduce it. Requiring
|
|
176
|
+
* assignability the other way as well makes the schema's output the contract's
|
|
177
|
+
* input and nothing else.
|
|
178
|
+
*
|
|
179
|
+
* Intersected onto the schema rather than mapped to `never`, so the compiler
|
|
180
|
+
* quotes the reason at the `input` property. `z.any()` passes in both
|
|
181
|
+
* directions, which is the one deliberate escape hatch; `z.unknown()` does
|
|
182
|
+
* not.
|
|
183
|
+
*/
|
|
184
|
+
type LambderMockInputPin<C, K extends keyof C, TSchema extends z.ZodType> = [
|
|
185
|
+
z.output<TSchema>
|
|
186
|
+
] extends [LambderMockInputOf<C, K>] ? ([LambderMockInputOf<C, K>] extends [z.output<TSchema>] ? unknown : {
|
|
187
|
+
"LambderMockApp: this input schema parses to less than the endpoint takes (an extra required field, or a narrower type), so the mock would answer 422 to payloads the server accepts": LambderMockInputOf<C, K>;
|
|
188
|
+
}) : {
|
|
189
|
+
"LambderMockApp: this input schema parses to something else than the endpoint's contract input": LambderMockInputOf<C, K>;
|
|
190
|
+
};
|
|
191
|
+
/** An entry written in full: the declarations restated and pinned, plus the handler. */
|
|
192
|
+
export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType> = LambderMockGuardsField<C, K> & LambderMockRateLimitField<C, K> & LambderMockIdempotencyField<C, K> & {
|
|
193
|
+
/**
|
|
194
|
+
* A schema to validate the posted payload against, which makes the mock
|
|
195
|
+
* answer 422 exactly as the server would. Optional, and deliberately the
|
|
196
|
+
* mock's own: the contract is a type, so the server's schemas do not exist
|
|
197
|
+
* at runtime on this side, and importing them would put the whole endpoint
|
|
198
|
+
* surface into the browser bundle. Restate the shape for the endpoints
|
|
199
|
+
* whose rejection path a test needs to exercise; leave it off and a bad
|
|
200
|
+
* payload reaches the handler, as it does today.
|
|
201
|
+
*
|
|
202
|
+
* Pinned to the contract's input all the same: the schema is the mock's,
|
|
203
|
+
* but what it parses to is the server's, in both directions (see
|
|
204
|
+
* LambderMockInputPin).
|
|
205
|
+
*/
|
|
206
|
+
input?: TInputSchema & LambderMockInputPin<C, K, TInputSchema>;
|
|
207
|
+
handler: LambderMockHandler<C, K, S, G>;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* What publicApi/sessionApi accept: a bare handler only for an endpoint the
|
|
211
|
+
* contract declares nothing for, the full options otherwise, so the form that
|
|
212
|
+
* cannot carry a restatement is unavailable exactly where one is owed.
|
|
213
|
+
*
|
|
214
|
+
* All three declarations, not guards alone. The three fields above make each
|
|
215
|
+
* restatement required INSIDE the options form, and the bare handler is the
|
|
216
|
+
* form that has no fields at all, so keying this on guards left every
|
|
217
|
+
* guardless endpoint free to drop its rate limit and its idempotency again:
|
|
218
|
+
* the handler ran twice for one key and a perMin limit never answered 429,
|
|
219
|
+
* which is the whole of what those two fields exist to prevent.
|
|
220
|
+
*/
|
|
221
|
+
export type LambderMockEntryInput<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType> = [
|
|
222
|
+
LambderContractGuardsOf<C, K> | LambderContractRateLimitOf<C, K> | LambderContractIdempotencyOf<C, K>
|
|
223
|
+
] extends [never] ? LambderMockHandler<C, K, S, G> | LambderMockEntryOptions<C, K, S, G, TInputSchema> : LambderMockEntryOptions<C, K, S, G, TInputSchema>;
|
|
224
|
+
/** One registry entry: the endpoint's definition as the pipeline runs it, and its handler (null when registered as not mocked). */
|
|
225
|
+
export type LambderMockEntry<C, K extends keyof C & string> = {
|
|
226
|
+
readonly name: K;
|
|
227
|
+
readonly mode: LambderApiMode;
|
|
228
|
+
readonly definition: LambderApiDefinition;
|
|
229
|
+
readonly handler: ((ctx: any) => Promise<unknown>) | null;
|
|
230
|
+
readonly notMockedReason: string | null;
|
|
231
|
+
};
|
|
232
|
+
/** The endpoint names a mock app may register under each mode. */
|
|
233
|
+
export type LambderMockPublicNames<C> = LambderContractKeysWithMode<C, "public">;
|
|
234
|
+
export type LambderMockSessionNames<C> = LambderContractKeysWithMode<C, "session">;
|
|
235
|
+
/** A slice: the entries of one module, keyed by endpoint name. */
|
|
236
|
+
export type LambderMockSlice<C, K extends keyof C & string> = {
|
|
237
|
+
readonly [P in K]: LambderMockEntry<C, P>;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* What restNotMocked(reason) hands register(): "every endpoint the slices
|
|
241
|
+
* beside me leave out is not mocked, for this reason".
|
|
242
|
+
*
|
|
243
|
+
* A one-field object rather than a slice, because it names no endpoint: it
|
|
244
|
+
* answers the names nothing else claimed, and which those are is only known
|
|
245
|
+
* once the other arguments have been read. Every check below filters it out
|
|
246
|
+
* before it reads a name, so the field it does carry is neither a stray nor
|
|
247
|
+
* half of a duplicate; the one clause it changes is completeness.
|
|
248
|
+
*/
|
|
249
|
+
export type LambderMockRestEntry = {
|
|
250
|
+
readonly restNotMockedReason: string;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* The names one slice holds; distributes over a union of slices.
|
|
254
|
+
*
|
|
255
|
+
* A slice typed with an index signature (`Record<string, LambderMockEntry>`,
|
|
256
|
+
* or a list built in a loop) holds `string` as its key type, which would
|
|
257
|
+
* subtract every name from the missing list and pass the completeness check
|
|
258
|
+
* while registering almost nothing. Such a slice names nothing the compiler
|
|
259
|
+
* can check, so it contributes nothing here and LambderMockUncheckableSlices
|
|
260
|
+
* reports it for what it is.
|
|
261
|
+
*
|
|
262
|
+
* The rest entry contributes nothing either, and for the opposite reason: the
|
|
263
|
+
* one key it carries is not an endpoint name, so reading it would report
|
|
264
|
+
* "restNotMockedReason" as a stray, and two rest entries as a duplicate of it.
|
|
265
|
+
*/
|
|
266
|
+
type LambderMockSliceNames<S> = S extends unknown ? (S extends LambderMockRestEntry ? never : (string extends keyof S ? never : keyof S & string)) : never;
|
|
267
|
+
/**
|
|
268
|
+
* Slices whose key set is an index signature rather than a finite list of
|
|
269
|
+
* names. Walks the tuple rather than distributing over `Slices[number]`,
|
|
270
|
+
* because the union loses which element held the index signature and the
|
|
271
|
+
* error should name it; LambderMockUncountableSlices rejects the lists this
|
|
272
|
+
* walk cannot cover.
|
|
273
|
+
*/
|
|
274
|
+
type LambderMockUncheckableSlices<Slices extends readonly unknown[]> = Slices extends readonly [infer Head, ...infer Tail] ? (string extends keyof Head ? Head : never) | LambderMockUncheckableSlices<Tail> : never;
|
|
275
|
+
/** Endpoints of the contract no slice covers. */
|
|
276
|
+
export type LambderMockMissingNames<C, Slices extends readonly unknown[]> = Exclude<keyof C & string, LambderMockSliceNames<Slices[number]>>;
|
|
277
|
+
/** True when one of register()'s arguments is the rest entry. */
|
|
278
|
+
type LambderMockHasRestEntry<Slices extends readonly unknown[]> = [
|
|
279
|
+
Extract<Slices[number], LambderMockRestEntry>
|
|
280
|
+
] extends [never] ? false : true;
|
|
281
|
+
/**
|
|
282
|
+
* The endpoints register() would leave unanswered: the ones no slice covers,
|
|
283
|
+
* unless a rest entry stands for them.
|
|
284
|
+
*
|
|
285
|
+
* The completeness clause and nothing else. An endpoint the rest entry answers
|
|
286
|
+
* is still an endpoint with no mock of its own, which is what
|
|
287
|
+
* LambderMockMissingNames says and why that one keeps its meaning; what the
|
|
288
|
+
* rest entry changes is whether leaving it out is a mistake.
|
|
289
|
+
*/
|
|
290
|
+
type LambderMockUncoveredNames<C, Slices extends readonly unknown[]> = LambderMockHasRestEntry<Slices> extends true ? never : LambderMockMissingNames<C, Slices>;
|
|
291
|
+
/** Names the slices carry that the contract does not declare. */
|
|
292
|
+
export type LambderMockStrayNames<C, Slices extends readonly unknown[]> = Exclude<LambderMockSliceNames<Slices[number]>, keyof C & string>;
|
|
293
|
+
/** Endpoints covered by more than one slice. */
|
|
294
|
+
export type LambderMockDuplicateNames<Slices extends readonly unknown[]> = Slices extends readonly [infer Head, ...infer Tail] ? (LambderMockSliceNames<Head> & LambderMockSliceNames<Tail[number]>) | LambderMockDuplicateNames<Tail> : never;
|
|
295
|
+
/**
|
|
296
|
+
* True for a slice list whose length the compiler does not know: an array
|
|
297
|
+
* type rather than a tuple, `length: number`.
|
|
298
|
+
*
|
|
299
|
+
* Every check below is written over a tuple, and an array type quietly
|
|
300
|
+
* disables all of them: the overlap and index-signature walks fall to their
|
|
301
|
+
* `never` base case on the first step, and completeness reduces to "the
|
|
302
|
+
* element type mentions these names", which one element satisfies as well as
|
|
303
|
+
* twenty. `const slices = [userMocks, orderMocks]` spread into register() is
|
|
304
|
+
* exactly that type, so the array form is refused rather than passed.
|
|
305
|
+
*/
|
|
306
|
+
type LambderMockUncountableSlices<Slices extends readonly unknown[]> = number extends Slices["length"] ? true : false;
|
|
307
|
+
/**
|
|
308
|
+
* What register() intersects its slices with: nothing when they cover the
|
|
309
|
+
* contract exactly once each, otherwise a shape naming what is wrong, which
|
|
310
|
+
* no slice list is assignable to. The key of that shape is the compiler's
|
|
311
|
+
* error message.
|
|
312
|
+
*/
|
|
313
|
+
export type LambderMockRegistryCheck<C, Slices extends readonly unknown[]> = LambderMockUncountableSlices<Slices> extends true ? {
|
|
314
|
+
"LambderMockApp: register() was given an array of slices rather than a fixed list, so it cannot see how many there are and cannot check the contract is covered. Pass the slices as arguments, register(a, b, c), or declare the list with `as const` before spreading it": Slices;
|
|
315
|
+
} : [LambderMockUncheckableSlices<Slices>] extends [never] ? ([LambderMockStrayNames<C, Slices>] extends [never] ? ([LambderMockUncoveredNames<C, Slices>] extends [never] ? ([LambderMockDuplicateNames<Slices>] extends [never] ? unknown : {
|
|
316
|
+
"LambderMockApp: these endpoints are mocked in more than one slice": LambderMockDuplicateNames<Slices>;
|
|
317
|
+
}) : {
|
|
318
|
+
"LambderMockApp: these endpoints have no mock (add one, mockApp.notMocked with a reason, or mockApp.restNotMocked(reason) for everything left out)": LambderMockUncoveredNames<C, Slices>;
|
|
319
|
+
}) : {
|
|
320
|
+
"LambderMockApp: these names are not endpoints of the contract": LambderMockStrayNames<C, Slices>;
|
|
321
|
+
}) : {
|
|
322
|
+
"LambderMockApp: a slice is typed with an index signature, so register() cannot see which endpoints it covers. Build it with mockApp.apiSlice(...) or annotate it as LambderMockSlice": LambderMockUncheckableSlices<Slices>;
|
|
323
|
+
};
|
|
324
|
+
/** A latency setting: milliseconds, a range to draw from, or a function of the api name. */
|
|
325
|
+
export type LambderMockLatency = number | {
|
|
326
|
+
min: number;
|
|
327
|
+
max: number;
|
|
328
|
+
} | ((apiName: string) => number);
|
|
329
|
+
/**
|
|
330
|
+
* An injected failure. Each is rendered by the same function the pipeline
|
|
331
|
+
* uses for the real thing, so an injected 429 carries the Retry-After a
|
|
332
|
+
* real one does. `network` rejects the transport; `timeout` waits for the
|
|
333
|
+
* caller's own abort (a call with no timeout configured waits for its
|
|
334
|
+
* external signal, or for ever, which is what a timeout is).
|
|
335
|
+
*/
|
|
336
|
+
export type LambderMockFailure = {
|
|
337
|
+
reason: "network";
|
|
338
|
+
} | {
|
|
339
|
+
reason: "timeout";
|
|
340
|
+
} | {
|
|
341
|
+
reason: "server";
|
|
342
|
+
} | {
|
|
343
|
+
reason: "refusal";
|
|
344
|
+
message?: LambderAppRefusalMessage | string;
|
|
345
|
+
statusCode?: LambderHttpStatusCode;
|
|
346
|
+
} | {
|
|
347
|
+
reason: "notAuthorized";
|
|
348
|
+
message?: LambderAppRefusalMessage | string;
|
|
349
|
+
} | {
|
|
350
|
+
reason: "sessionExpired";
|
|
351
|
+
} | {
|
|
352
|
+
reason: "versionExpired";
|
|
353
|
+
} | {
|
|
354
|
+
reason: "rateLimited";
|
|
355
|
+
retryAfterSeconds?: number;
|
|
356
|
+
message?: LambderAppRefusalMessage;
|
|
357
|
+
};
|
|
358
|
+
/** Every sibling in the package spells this `reason`: an outcome's, a transport failure's, an invoke failure's. */
|
|
359
|
+
export type LambderMockFailureReason = LambderMockFailure["reason"];
|
|
360
|
+
/**
|
|
361
|
+
* How one call ended, as the runtime saw it. `passthrough` is the MSW
|
|
362
|
+
* adapter's: the runtime answered nothing and the request went on to MSW's
|
|
363
|
+
* other handlers and the network.
|
|
364
|
+
*/
|
|
365
|
+
export type LambderMockOutcome = "ok" | "refusal" | "notAuthorized" | "sessionExpired" | "versionExpired" | "rateLimited" | "replayed" | "validation" | "notMocked" | "unknownApi" | "crash" | "injected" | "passthrough";
|
|
366
|
+
export type LambderMockRequestEvent = {
|
|
367
|
+
phase: "request";
|
|
368
|
+
id: number;
|
|
369
|
+
apiName: string;
|
|
370
|
+
/** The endpoint's mode from the registry; null for a name the registry does not know. */
|
|
371
|
+
mode: LambderApiMode | null;
|
|
372
|
+
payload: unknown;
|
|
373
|
+
guardInputs: Record<string, unknown> | undefined;
|
|
374
|
+
/** Exactly as posted, so `unknown`: the key is client data and only the idempotency engine judges it. */
|
|
375
|
+
idempotencyKey: unknown;
|
|
376
|
+
version: string | null;
|
|
377
|
+
headers: Record<string, string>;
|
|
378
|
+
/** True when the request carried a session cookie. */
|
|
379
|
+
hasSessionCookie: boolean;
|
|
380
|
+
at: number;
|
|
381
|
+
};
|
|
382
|
+
export type LambderMockResponseEvent = {
|
|
383
|
+
phase: "response";
|
|
384
|
+
id: number;
|
|
385
|
+
apiName: string;
|
|
386
|
+
mode: LambderApiMode | null;
|
|
387
|
+
durationMs: number;
|
|
388
|
+
/** Absent when the transport rejected (an injected network failure or timeout), and on a passthrough. */
|
|
389
|
+
statusCode: number | null;
|
|
390
|
+
/**
|
|
391
|
+
* The answer's headers, with every Set-Cookie value replaced by
|
|
392
|
+
* "[redacted]": the cookie's name and attributes still read (did this
|
|
393
|
+
* call start a session, did it clear one), while the session token they
|
|
394
|
+
* carry stays out of a log a dev panel renders and a test snapshots.
|
|
395
|
+
*/
|
|
396
|
+
headers: Record<string, string[]>;
|
|
397
|
+
/** The parsed envelope, when the answer was one. */
|
|
398
|
+
envelope: LambderApiEnvelopeBody<unknown> | null;
|
|
399
|
+
outcome: LambderMockOutcome;
|
|
400
|
+
guardsRun: string[];
|
|
401
|
+
/** The crash, or the injected transport failure. */
|
|
402
|
+
error?: Error;
|
|
403
|
+
at: number;
|
|
404
|
+
};
|
|
405
|
+
export type LambderMockCallEvent = LambderMockRequestEvent | LambderMockResponseEvent;
|
|
406
|
+
/** One completed call, for assertions and panels: the response event plus what was asked. */
|
|
407
|
+
export type LambderMockCallRecord = LambderMockResponseEvent & {
|
|
408
|
+
payload: unknown;
|
|
409
|
+
guardInputs: Record<string, unknown> | undefined;
|
|
410
|
+
};
|
|
411
|
+
export type LambderMockListener = (event: LambderMockCallEvent) => void;
|
|
412
|
+
/**
|
|
413
|
+
* The mock's rate-limit policies, keyed by name. Bound to the mock's own call
|
|
414
|
+
* context so a custom key handler is typed for the runtime that will actually
|
|
415
|
+
* call it: a handler built with the server's lambderRateLimitKey() reads the
|
|
416
|
+
* render context's ip/method/path, none of which the mock context has, so it
|
|
417
|
+
* would compile here and then key every caller identically. Build these with
|
|
418
|
+
* `rateLimitKey` from initLambderMock().
|
|
419
|
+
*/
|
|
420
|
+
export type LambderMockRateLimitPolicies<S = any> = Record<string, LambderApiRateLimitPolicyConfig<LambderMockCallContext<S>>>;
|
|
421
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of the mock runtime: what a registry entry is, what a handler
|
|
3
|
+
* receives, how a registry is checked against a contract, and what the
|
|
4
|
+
* subscription emits. Everything a consumer needs at runtime it restates in
|
|
5
|
+
* the builders; these types pin every restatement to the contract type, so
|
|
6
|
+
* the contract stays a type-only import in the consuming app.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LambderMockListener } from "./LambderMockTypes.js";
|
|
2
|
+
export type LambderMockConsoleLoggerOptions = {
|
|
3
|
+
/** Log the request payload and the answer's envelope beside each line. Default: false. */
|
|
4
|
+
payloads?: boolean;
|
|
5
|
+
/** With payloads on, group each call's detail under a collapsed console group. Default: true. */
|
|
6
|
+
collapsed?: boolean;
|
|
7
|
+
/** Where to write. Default: console. */
|
|
8
|
+
console?: Pick<Console, "log" | "group" | "groupCollapsed" | "groupEnd">;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A ready-made subscriber: one line per completed call, naming the endpoint,
|
|
12
|
+
* how it ended, the status and the time it took; with `payloads` on, the
|
|
13
|
+
* request payload and the answer's envelope under a collapsed group. So
|
|
14
|
+
* `mockApp.subscribe("console", lambderMockConsoleLogger())` logs everything.
|
|
15
|
+
*/
|
|
16
|
+
export declare const lambderMockConsoleLogger: (options?: LambderMockConsoleLoggerOptions) => LambderMockListener;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ready-made subscriber: one line per completed call, naming the endpoint,
|
|
3
|
+
* how it ended, the status and the time it took; with `payloads` on, the
|
|
4
|
+
* request payload and the answer's envelope under a collapsed group. So
|
|
5
|
+
* `mockApp.subscribe("console", lambderMockConsoleLogger())` logs everything.
|
|
6
|
+
*/
|
|
7
|
+
export const lambderMockConsoleLogger = (options = {}) => {
|
|
8
|
+
const output = options.console ?? console;
|
|
9
|
+
const collapsed = options.collapsed ?? true;
|
|
10
|
+
const payloads = new Map();
|
|
11
|
+
return (event) => {
|
|
12
|
+
if (event.phase === "request") {
|
|
13
|
+
if (options.payloads)
|
|
14
|
+
payloads.set(event.id, event.payload);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const status = event.statusCode === null ? "no answer" : String(event.statusCode);
|
|
18
|
+
const line = `[lambder mock] ${event.apiName} → ${event.outcome} (${status}, ${event.durationMs}ms)`;
|
|
19
|
+
if (!options.payloads) {
|
|
20
|
+
output.log(line);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const payload = payloads.get(event.id);
|
|
24
|
+
payloads.delete(event.id);
|
|
25
|
+
(collapsed ? output.groupCollapsed : output.group).call(output, line);
|
|
26
|
+
output.log("payload", payload);
|
|
27
|
+
if (event.envelope)
|
|
28
|
+
output.log("envelope", event.envelope);
|
|
29
|
+
if (event.guardsRun.length)
|
|
30
|
+
output.log("guards", event.guardsRun.join(", "));
|
|
31
|
+
if (event.error)
|
|
32
|
+
output.log("error", event.error);
|
|
33
|
+
output.groupEnd();
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LambderApiAnswer } from "../api/LambderApiAnswer.js";
|
|
2
|
+
import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The fields of an invoke's synthesized event this transport reads, declared
|
|
5
|
+
* structurally rather than imported as APIGatewayProxyEventV2.
|
|
6
|
+
*
|
|
7
|
+
* `lambder/mock` is browser-safe, and its type graph is part of that claim: a
|
|
8
|
+
* type-only import of the invoke caller pulled `aws-lambda` and
|
|
9
|
+
* `@aws-sdk/client-lambda` into the .d.ts graph of the mock entry, so a
|
|
10
|
+
* frontend compiling without `skipLibCheck` and without those @types got
|
|
11
|
+
* errors from inside node_modules for a module it never loads. Every field a
|
|
12
|
+
* real event carries is optional here and no field is required, so a real
|
|
13
|
+
* event is assignable and the returned function still fits
|
|
14
|
+
* LambderInvokeTransport wherever a caller expects one.
|
|
15
|
+
*/
|
|
16
|
+
export type LambderMockInvokeEvent = {
|
|
17
|
+
body?: string | undefined;
|
|
18
|
+
isBase64Encoded?: boolean | undefined;
|
|
19
|
+
headers?: Record<string, string | undefined> | undefined;
|
|
20
|
+
cookies?: string[] | undefined;
|
|
21
|
+
requestContext?: {
|
|
22
|
+
http?: {
|
|
23
|
+
sourceIp?: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
domainName?: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
};
|
|
28
|
+
/** What the callee answers with: the Lambda response object LambderInvokeCaller decodes. */
|
|
29
|
+
export type LambderMockInvokeResult = {
|
|
30
|
+
functionError: string | null;
|
|
31
|
+
result: {
|
|
32
|
+
statusCode: number;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
cookies?: string[];
|
|
35
|
+
body: string;
|
|
36
|
+
isBase64Encoded: boolean;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The mock app as the callee of a LambderInvokeCaller: the invoke's
|
|
41
|
+
* synthesized event is read the way the callee's createContext would read
|
|
42
|
+
* it, and the answer goes back as the Lambda response object the caller
|
|
43
|
+
* decodes. So a server test can point its typed invoke caller at a mock of
|
|
44
|
+
* the function it depends on, with the same registry a browser test uses.
|
|
45
|
+
*/
|
|
46
|
+
export declare const lambderMockInvokeTransport: (mockApp: {
|
|
47
|
+
handleRequest(request: LambderApiRequest): Promise<LambderApiAnswer>;
|
|
48
|
+
}) => ((event: LambderMockInvokeEvent, options: {
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}) => Promise<LambderMockInvokeResult>);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
2
|
+
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
3
|
+
import { base64ToText } from "../shared/util/LambderBase64.js";
|
|
4
|
+
import { normalizeClientIp } from "../shared/util/LambderClientIp.js";
|
|
5
|
+
/**
|
|
6
|
+
* The mock app as the callee of a LambderInvokeCaller: the invoke's
|
|
7
|
+
* synthesized event is read the way the callee's createContext would read
|
|
8
|
+
* it, and the answer goes back as the Lambda response object the caller
|
|
9
|
+
* decodes. So a server test can point its typed invoke caller at a mock of
|
|
10
|
+
* the function it depends on, with the same registry a browser test uses.
|
|
11
|
+
*/
|
|
12
|
+
export const lambderMockInvokeTransport = (mockApp) => async (event, { signal }) => {
|
|
13
|
+
const rawBody = event.body ?? "";
|
|
14
|
+
const body = event.isBase64Encoded ? base64ToText(rawBody) : rawBody;
|
|
15
|
+
let post = {};
|
|
16
|
+
try {
|
|
17
|
+
post = JSON.parse(body || "{}") ?? {};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
post = {};
|
|
21
|
+
}
|
|
22
|
+
const headers = lowercaseHeaderNames(event.headers);
|
|
23
|
+
const cookies = cookieValuesByName(event.cookies ?? []);
|
|
24
|
+
// The address the synthesized event carries in sourceIp, as the server's
|
|
25
|
+
// createContext reads it; no forwarding header is trusted here either,
|
|
26
|
+
// so a per-IP limit keys the same address under both adapters.
|
|
27
|
+
const request = readApiEnvelope(post, {
|
|
28
|
+
headers, cookies,
|
|
29
|
+
ip: normalizeClientIp(event.requestContext?.http?.sourceIp ?? ""),
|
|
30
|
+
host: headers.host || event.requestContext?.domainName || "lambder-invoke",
|
|
31
|
+
...(signal ? { signal } : {}),
|
|
32
|
+
});
|
|
33
|
+
if (!request) {
|
|
34
|
+
return { functionError: null, result: { statusCode: 404, headers: { "content-type": "text/plain; charset=utf-8" }, body: "Not found.", isBase64Encoded: false } };
|
|
35
|
+
}
|
|
36
|
+
const answer = await mockApp.handleRequest(request);
|
|
37
|
+
const singleHeaders = {};
|
|
38
|
+
for (const [key, values] of Object.entries(answer.headers)) {
|
|
39
|
+
if (key.toLowerCase() !== "set-cookie")
|
|
40
|
+
singleHeaders[key] = values.join(", ");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
functionError: null,
|
|
44
|
+
result: {
|
|
45
|
+
statusCode: answer.statusCode,
|
|
46
|
+
headers: singleHeaders,
|
|
47
|
+
cookies: getAnswerHeader(answer.headers, "Set-Cookie") ?? [],
|
|
48
|
+
body: answer.body,
|
|
49
|
+
isBase64Encoded: answer.isBodyBase64 ?? false,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|