lambder 11.0.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 +154 -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/core/LambderPublicFiles.d.ts +8 -1
- package/dist/core/LambderPublicFiles.js +1 -1
- 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
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptio
|
|
|
21
21
|
import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusalNamesOf } from "../api/LambderApiGuards.js";
|
|
22
22
|
import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
|
|
23
23
|
import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
|
|
24
|
-
import type { LambderContractEntry, LambderJsonOf
|
|
24
|
+
import type { LambderContractEntry, LambderJsonOf } from "../shared/wire/LambderApiContract.js";
|
|
25
|
+
import { type LambderActionNamesCheck, type LambderApiDeclaration, type LambderApiDeclarations, type LambderApiGroup, type LambderApiModeOf, type LambderAppTypes, type LambderContractOfGroups, type LambderGroupNameCheck, type LambderGroupPartsCheck, type LambderMergedParts, type LambderLazyApiGroup, type LambderPlainAppTypes, type LambderPublicSessionPolicyCheck, type LambderRegisteredGroupsCheck, type LambderRegistrableApiGroup, type LambderSessionModeCheck } from "../api/LambderApiDeclarations.js";
|
|
25
26
|
import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
26
27
|
import type { LambderReadonlyDeep, MaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
27
28
|
import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderRouteHandler, type LambderInputValidationHandler, type LambderFallbackHandler, type LambderGlobalErrorHandler, type LambderAfterRenderHook, type LambderBeforeRenderHook, type LambderFallbackHook, type LambderActionTools, type LambderCreateOptions, type LambderGivenOption, type LambderHandler, type LambderNestedOptionChecks, type LambderObjectOutputCheck, type LambderPayloadSliceCheck, type LambderRequirableGuardsField, type LambderSessionEnabledInstance, type LambderSessionRouteHandler } from "./LambderCreateOptions.js";
|
|
@@ -39,36 +40,41 @@ export type LambderInstanceBackendSwap = LambderPipelineBackendSwap & {
|
|
|
39
40
|
* because its parameter is the class, and an options module that names the
|
|
40
41
|
* class cannot be read without it.
|
|
41
42
|
*/
|
|
42
|
-
export type LambderCreatedHook = (lambderInstance: Lambder<any, any
|
|
43
|
+
export type LambderCreatedHook = (lambderInstance: Lambder<any, any>) => void | Promise<void>;
|
|
43
44
|
/** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
|
|
44
45
|
type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
|
|
46
|
+
/**
|
|
47
|
+
* What an endpoint's handler is handed: the session context when one of its
|
|
48
|
+
* guards needs a session (so ctx.session is typed present), the plain one
|
|
49
|
+
* otherwise, with the payload, the guards' data and a refuse() typed to the
|
|
50
|
+
* codes the endpoint may refuse with.
|
|
51
|
+
*/
|
|
52
|
+
type LambderApiHandlerContext<TApp extends LambderAppTypes, TPayload, TGuardsOpt, TRefusalsOpt> = (LambderApiModeOf<TApp["guards"], TGuardsOpt> extends "session" ? LambderSessionRenderContext<TPayload, TApp["session"], Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["policies"]> : LambderRenderContext<TPayload, Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["session"], TApp["policies"]>) & {
|
|
53
|
+
refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
|
|
54
|
+
};
|
|
45
55
|
/**
|
|
46
56
|
* Main Lambder class for building type-safe serverless APIs. Create
|
|
47
57
|
* instances with initLambder<SessionData>().create({...}) (see below): the
|
|
48
58
|
* whole configuration, including the typed policy layer, is given at
|
|
49
|
-
* construction
|
|
59
|
+
* construction. Endpoints are declared as values on the instance
|
|
60
|
+
* (defineApi, defineApiGroup) and registered in one registerApiGroups()
|
|
61
|
+
* call; routes, hooks and actions chain.
|
|
50
62
|
*
|
|
51
|
-
* @typeParam
|
|
52
|
-
* @typeParam _TContract - @internal
|
|
53
|
-
* @typeParam _TRateLimitPolicies - @internal Inferred from create()'s rateLimits.policies (do not pass manually)
|
|
54
|
-
* @typeParam _TGuards - @internal Guard metadata map inferred from create()'s guards (do not pass manually)
|
|
55
|
-
* @typeParam _TIdempotencyEnabled - @internal True when create() received idempotency (do not pass manually)
|
|
56
|
-
* @typeParam _TSessionGuardsRequired - @internal True when create() received requireSessionApiGuards (do not pass manually)
|
|
57
|
-
* @typeParam _TPublicGuardsRequired - @internal True when create() received requirePublicApiGuards (do not pass manually)
|
|
58
|
-
* @typeParam _TSessionsEnabled - @internal True when create() received the session option (do not pass manually). Defaults to true, unlike its siblings, so a plugin annotating its parameter as the bare Lambder<SessionData> can still register session APIs. create() knows the option and supplies the false; `new Lambder(...)` relies on the registration-time throw alone.
|
|
59
|
-
* @typeParam _TRefusals - @internal The refusal vocabulary declareRefusals() gave the init (do not pass manually)
|
|
60
|
-
* @typeParam _TRefusalCodesRequired - @internal True when declareRefusals() received requireCodes (do not pass manually)
|
|
63
|
+
* @typeParam TApp - @internal What create() configured, as LambderAppTypes: the session data, policies, guards, idempotency, the guard requirement, sessions and the refusal vocabulary (do not pass manually)
|
|
64
|
+
* @typeParam _TContract - @internal The contract of the groups registered so far (do not pass manually)
|
|
61
65
|
*
|
|
62
66
|
* @example
|
|
63
67
|
* ```typescript
|
|
64
68
|
* interface SessionData { userId: string; role: string; }
|
|
65
69
|
*
|
|
66
|
-
* const
|
|
67
|
-
*
|
|
68
|
-
*
|
|
70
|
+
* const app = initLambder<SessionData>().create({ apiPath: '/api', session, guards });
|
|
71
|
+
* const userApis = app.defineApiGroup("users", {
|
|
72
|
+
* get: app.defineApi({ input: z.object({...}), output: z.object({...}), guards: "signedIn" }, handler),
|
|
73
|
+
* });
|
|
74
|
+
* const lambder = app.registerApiGroups(userApis);
|
|
69
75
|
* ```
|
|
70
76
|
*/
|
|
71
|
-
export default class Lambder<
|
|
77
|
+
export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes, _TContract extends Record<string, any> = {}> {
|
|
72
78
|
apiPath: string;
|
|
73
79
|
/** Stamped on every API answer's envelope as apiVersion. Informational: a client's staleness is judged per endpoint by its signature, see apiSignatures(). */
|
|
74
80
|
apiVersion: null | string;
|
|
@@ -78,24 +84,35 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
78
84
|
* Type property for extracting the API contract: every registered API's
|
|
79
85
|
* input, output, mode and declared options, as a client calls it.
|
|
80
86
|
*
|
|
81
|
-
* A small app's client imports it as it is.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* this property off the exported instance and writes the contract out as
|
|
85
|
-
* plain types for such a client to import instead.
|
|
87
|
+
* A small app's client imports it as it is. A large app's client reads
|
|
88
|
+
* the plain types writeApiContract (lambder/build) prints from this
|
|
89
|
+
* property instead, which cost it nothing to resolve.
|
|
86
90
|
*
|
|
87
91
|
* @example
|
|
88
92
|
* ```typescript
|
|
89
|
-
* export const lambder =
|
|
93
|
+
* export const lambder = lambderApp.registerApiGroups(userApis, orderApis);
|
|
90
94
|
* export type ApiContractType = typeof lambder.ApiContract;
|
|
91
95
|
* ```
|
|
92
96
|
*/
|
|
93
97
|
readonly ApiContract: _TContract;
|
|
98
|
+
/**
|
|
99
|
+
* Type property for what create() configured (LambderAppTypes): the
|
|
100
|
+
* session data, policies, guards and the rest, for code that takes any
|
|
101
|
+
* instance and needs one of them (lambderTestApp reads the session data
|
|
102
|
+
* type here). Like ApiContract, it has no value.
|
|
103
|
+
*/
|
|
104
|
+
readonly AppTypes: TApp;
|
|
94
105
|
private actionList;
|
|
95
106
|
/** The API core: the pipeline every API call runs through, shared in shape with the mock runtime. */
|
|
96
107
|
private readonly pipeline;
|
|
97
|
-
/** Every registered API by name:
|
|
108
|
+
/** Every registered API by name: what dispatch, apiSignatures() and apiOptionEntries() read. */
|
|
98
109
|
private readonly apiDefinitions;
|
|
110
|
+
/** What answers each registered API: its output schema, compression setting and handler. */
|
|
111
|
+
private readonly apiHandlers;
|
|
112
|
+
/** Every group name registered, lazy or not: the duplicate-group check. */
|
|
113
|
+
private readonly apiGroupNames;
|
|
114
|
+
/** The lazy groups not loaded yet, each with its load in progress, if one is. */
|
|
115
|
+
private readonly lazyApiGroups;
|
|
99
116
|
/** The guards given at creation, merged into one map, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
|
|
100
117
|
private readonly guards;
|
|
101
118
|
/** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
@@ -117,7 +134,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
117
134
|
private eventActionList;
|
|
118
135
|
private corsConfig;
|
|
119
136
|
private finalizeOptions;
|
|
120
|
-
|
|
137
|
+
/** Whether every endpoint has to declare guards (create()'s requireApiGuards). */
|
|
138
|
+
private readonly requireApiGuards;
|
|
121
139
|
/** Whether every refusal an API answers with has to name a code (declareRefusals's requireCodes). */
|
|
122
140
|
private readonly requireRefusalCodes;
|
|
123
141
|
/** Told what a request threw, beside whatever answers it; null outside a test. See LAMBDER_CRASH_WATCH. */
|
|
@@ -128,8 +146,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
128
146
|
private readonly contextTools;
|
|
129
147
|
private readonly trustedClientIpHeaders;
|
|
130
148
|
private readonly trustedHostHeaders;
|
|
131
|
-
|
|
132
|
-
constructor(given?: LambderCreateOptions<TSessionData>);
|
|
149
|
+
constructor(given?: LambderCreateOptions<TApp["session"]>);
|
|
133
150
|
setRouteFallbackHandler(routeFallbackHandler: LambderFallbackHandler): this;
|
|
134
151
|
setApiFallbackHandler(apiFallbackHandler: LambderFallbackHandler): this;
|
|
135
152
|
setApiInputValidationErrorHandler(apiInputValidationErrorHandler: LambderInputValidationHandler): this;
|
|
@@ -163,45 +180,33 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
163
180
|
* res.templateFile (markers optional) with no-cache.
|
|
164
181
|
*/
|
|
165
182
|
serveIndexHtml(handler?: LambderFallbackHandler, options?: LambderIndexHtmlOptions): this;
|
|
166
|
-
addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {},
|
|
183
|
+
addRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, LambderPathParamsOf<TPath>, {}, TApp["session"], TApp["policies"]>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
|
|
167
184
|
addRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderRouteHandler): this;
|
|
168
|
-
addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any,
|
|
169
|
-
addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
* body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
|
|
187
|
-
* compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
|
|
188
|
-
* cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
|
|
189
|
-
* both ends. A transport setting of this server's, not part of the API's contract.
|
|
190
|
-
*/
|
|
191
|
-
compress?: boolean | "auto";
|
|
192
|
-
} & LambderRequirableGuardsField<_TPublicGuardsRequired, TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<_TGuards, _TRateLimitPolicies, TGuardsOpt, TRateOpt, z.input<TInput>>,
|
|
193
|
-
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
194
|
-
handler: (ctx: LambderRenderContext<z.infer<TInput>, Record<string, string>, LambderGuardDataOf<_TGuards, TGuardsOpt>, TSessionData, _TRateLimitPolicies> & {
|
|
195
|
-
refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>, _TRefusalCodesRequired>;
|
|
196
|
-
}) => MaybePromise<TAnswer>): Lambder<TSessionData, LambderMergeContract<_TContract, TName, LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, "public", LambderGuardInputsOf<_TGuards, TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<_TRefusals, LambderApiRefusalCodes<_TRefusals, _TGuards, TRefusalsOpt, TGuardsOpt>>>>, _TRateLimitPolicies, _TGuards, _TIdempotencyEnabled, _TSessionGuardsRequired, _TPublicGuardsRequired, _TSessionsEnabled, _TRefusals, _TRefusalCodesRequired>;
|
|
197
|
-
addSessionApi<TName extends string, TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<_TRateLimitPolicies, true> = never, const TGuardsOpt extends LambderGuardsOption<_TGuards, true> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<_TRefusals> = never>(name: TName, schema: {
|
|
185
|
+
addSessionRoute<TPath extends LambderRoutePath>(condition: TPath, actionFn: ((ctx: LambderSessionRenderContext<any, TApp["session"], LambderPathParamsOf<TPath>, {}, TApp["policies"]>, resolver: LambderResolver) => MaybePromise<LambderResponse>) & LambderSessionEnabledInstance<TApp["sessions"]>): this;
|
|
186
|
+
addSessionRoute(condition: RegExp | LambderRouteConditionFn | LambderRouteMatcher, actionFn: LambderSessionRouteHandler<TApp["session"]> & LambderSessionEnabledInstance<TApp["sessions"]>): this;
|
|
187
|
+
/**
|
|
188
|
+
* Declares one endpoint, typed on this instance's types: its input and
|
|
189
|
+
* output schemas, its guards, rate limits, idempotency and refusals, and
|
|
190
|
+
* its handler. Nothing is registered until the declaration is put in a
|
|
191
|
+
* group (defineApiGroup) and the group is registered (registerApiGroups).
|
|
192
|
+
*
|
|
193
|
+
* The guards say who may call it, and so its mode: an endpoint whose
|
|
194
|
+
* guards include one that needs a session (`session: true`) is a session
|
|
195
|
+
* endpoint, its session read before the guards run and a call without
|
|
196
|
+
* one answered sessionExpired, and its handler's ctx.session is typed
|
|
197
|
+
* present. Any other endpoint is public.
|
|
198
|
+
*
|
|
199
|
+
* An arrow property rather than a method, so an app can hand it out
|
|
200
|
+
* detached: `export const { defineApi, defineApiGroup } = lambderApp;`.
|
|
201
|
+
*/
|
|
202
|
+
readonly defineApi: <TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<TApp["policies"]> = never, const TGuardsOpt extends LambderGuardsOption<TApp["guards"]> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<TApp["refusals"]> = never>(options: {
|
|
198
203
|
input: TInput;
|
|
199
204
|
output: TOutput;
|
|
200
205
|
} & {
|
|
201
|
-
/** Named rate limits, checked in declared order within their phase (per ip before the session read, per session before the guards, a custom key after the guards and input validation): a name, a list of names, or a { name: true | override } map (windows overridable on perApi budgets, refusal on any). The first exceeded one refuses (429 envelope + Retry-After); attempts count on every counter checked before it. */
|
|
206
|
+
/** Named rate limits, checked in declared order within their phase (per ip before the session read, per session before the guards, a custom key after the guards and input validation): a name, a list of names, or a { name: true | override } map (windows overridable on perApi budgets, refusal on any). The first exceeded one refuses (429 envelope + Retry-After); attempts count on every counter checked before it. A per-session policy needs one of the endpoint's guards to need a session. */
|
|
202
207
|
rateLimit?: TRateOpt;
|
|
203
208
|
/** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
|
|
204
|
-
idempotency?:
|
|
209
|
+
idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
|
|
205
210
|
/**
|
|
206
211
|
* The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
|
|
207
212
|
* non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
|
|
@@ -216,19 +221,96 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
216
221
|
* both ends. A transport setting of this server's, not part of the API's contract.
|
|
217
222
|
*/
|
|
218
223
|
compress?: boolean | "auto";
|
|
219
|
-
} & LambderRequirableGuardsField<
|
|
224
|
+
} & LambderRequirableGuardsField<TApp["guardsRequired"], TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<TApp["guards"], TApp["policies"], TGuardsOpt, TRateOpt, z.input<TInput>> & LambderPublicSessionPolicyCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["policies"], TRateOpt> & LambderSessionModeCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["sessions"]>,
|
|
220
225
|
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
221
|
-
handler: (ctx:
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
handler: (ctx: LambderApiHandlerContext<TApp, z.infer<TInput>, TGuardsOpt, TRefusalsOpt>) => MaybePromise<TAnswer>) => LambderApiDeclaration<LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, LambderApiModeOf<TApp["guards"], TGuardsOpt>, LambderGuardInputsOf<TApp["guards"], TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>>>;
|
|
227
|
+
/**
|
|
228
|
+
* Gathers endpoints into a named group: each is registered as
|
|
229
|
+
* `name.action` and called at `{apiPath}/{name}/{action}`, and every
|
|
230
|
+
* Lambder caller reaches it as `caller.name.action(input)`. A group's
|
|
231
|
+
* name and its actions are identifiers, and a group may not take a name
|
|
232
|
+
* a caller already answers for (see LAMBDER_RESERVED_GROUP_NAMES).
|
|
233
|
+
*
|
|
234
|
+
* A group declared across files takes each file's part:
|
|
235
|
+
* `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action
|
|
236
|
+
* two parts declare is refused, at compile time and at startup, rather
|
|
237
|
+
* than one silently replacing the other as an object spread would.
|
|
238
|
+
*/
|
|
239
|
+
readonly defineApiGroup: <const TName extends string, const TParts extends readonly [LambderApiDeclarations, ...LambderApiDeclarations[]]>(name: TName & LambderGroupNameCheck<TName>, ...parts: TParts & LambderGroupPartsCheck<TParts> & { [TIndex in keyof TParts]: LambderActionNamesCheck<TParts[TIndex]>; }) => LambderApiGroup<TName, LambderMergedParts<TParts>>;
|
|
240
|
+
/**
|
|
241
|
+
* A group loaded on the first call to one of its endpoints:
|
|
242
|
+
* `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
|
|
243
|
+
* A cold start then parses none of it, nor anything only it imports,
|
|
244
|
+
* until a request calls it; the contract is the loaded group's, read off
|
|
245
|
+
* its type. The group loaded must carry the same name, which the type
|
|
246
|
+
* checks and loading checks again. `loadApiGroups()` loads every group,
|
|
247
|
+
* for a build step or a boot check that has to see them all.
|
|
248
|
+
*/
|
|
249
|
+
readonly lazyApiGroup: <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
|
|
250
|
+
/**
|
|
251
|
+
* Registers groups of endpoints, as one contract: `typeof
|
|
252
|
+
* lambder.ApiContract` then maps every `group.action` to its entry, one
|
|
253
|
+
* flat object type the api-contract generator prints as it is. Register
|
|
254
|
+
* every group in one call where you can: the contract of one call is one
|
|
255
|
+
* mapped type, and a second call intersects it with the first.
|
|
256
|
+
*
|
|
257
|
+
* The call's groups take their place in the first-match chain here, as
|
|
258
|
+
* a route does: a route or action registered before them sees their calls
|
|
259
|
+
* first, one registered after them never does. The beforeRender hooks run
|
|
260
|
+
* before a lazy group loads, so a hook that answers a request spares it
|
|
261
|
+
* the import; one that throws a refusal or charges a per-API budget loads
|
|
262
|
+
* it (see registeredDefinitionOf).
|
|
263
|
+
*
|
|
264
|
+
* Refused at compile time: a group typed `any`, or a lazy one loading a
|
|
265
|
+
* group typed `any`, or a group whose endpoints are (its endpoints would
|
|
266
|
+
* be `any` to every client), and a group name given twice. Refused at
|
|
267
|
+
* registration, which is at startup for a group and at its first call for
|
|
268
|
+
* a lazy one: everything registration refuses of one endpoint (an unknown
|
|
269
|
+
* guard or policy, a missing guard where requireApiGuards is on, a
|
|
270
|
+
* refusal code outside the vocabulary).
|
|
271
|
+
*/
|
|
272
|
+
registerApiGroups<const TGroups extends readonly LambderRegistrableApiGroup[]>(this: LambderRegisteredGroupsCheck<TGroups, Lambder<TApp, _TContract>>, ...groups: TGroups): Lambder<TApp, _TContract & LambderContractOfGroups<TGroups>>;
|
|
273
|
+
/**
|
|
274
|
+
* Loads every lazy group not loaded yet, registering its endpoints: what
|
|
275
|
+
* a build step digesting the signatures, a generator writing the options
|
|
276
|
+
* or a boot check has to do before it sees every endpoint. A group whose
|
|
277
|
+
* registration fails rejects here, which is the point of calling it.
|
|
278
|
+
*/
|
|
279
|
+
loadApiGroups(): Promise<void>;
|
|
224
280
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* they give the handler.
|
|
281
|
+
* An API call to a registered group, its hooks run: its group loaded
|
|
282
|
+
* first when it is a lazy one no call has reached yet, then its endpoint
|
|
283
|
+
* run. An action the group does not have is answered as any unmatched API
|
|
284
|
+
* call is, fallback hooks first.
|
|
230
285
|
*/
|
|
231
|
-
private
|
|
286
|
+
private dispatchApiCall;
|
|
287
|
+
/**
|
|
288
|
+
* The definition of the endpoint a call names, its group loaded first
|
|
289
|
+
* when it is a lazy one no call has reached yet; undefined for a name no
|
|
290
|
+
* registered group declares. Everything that reads an endpoint's
|
|
291
|
+
* declarations for a call asks here (its dispatch, a refusal a hook
|
|
292
|
+
* throws on the way to it, a per-API budget a hook charges), so a lazy
|
|
293
|
+
* group answers each of them as an eager one does.
|
|
294
|
+
*/
|
|
295
|
+
private registeredDefinitionOf;
|
|
296
|
+
/** The endpoints of one group, each checked as `group.action`; nothing is registered until they are committed. */
|
|
297
|
+
private prepareApiGroup;
|
|
298
|
+
/**
|
|
299
|
+
* A lazy group, loaded once: its import, checked to be the group it was
|
|
300
|
+
* registered as, then registered. Concurrent first calls share one load;
|
|
301
|
+
* a failed load is forgotten, so the next call tries again rather than
|
|
302
|
+
* answering every later call with the first failure.
|
|
303
|
+
*/
|
|
304
|
+
private loadLazyApiGroup;
|
|
305
|
+
/**
|
|
306
|
+
* What registering an endpoint takes: the checks that can refuse it and
|
|
307
|
+
* its mode read off its guards, into the definition apiSignatures()
|
|
308
|
+
* digests, beside its handler. Nothing is recorded here (commitApi does
|
|
309
|
+
* that), so a refusal anywhere in a registration leaves nothing behind.
|
|
310
|
+
*/
|
|
311
|
+
private prepareApi;
|
|
312
|
+
/** Records a checked endpoint: its definition, its refusals option as written, and what answers it. */
|
|
313
|
+
private commitApi;
|
|
232
314
|
addHook(hookEvent: 'created', hookFn: LambderCreatedHook, priority?: number): this;
|
|
233
315
|
addHook(hookEvent: 'beforeRender', hookFn: LambderBeforeRenderHook, priority?: number): this;
|
|
234
316
|
addHook(hookEvent: 'afterRender', hookFn: LambderAfterRenderHook, priority?: number): this;
|
|
@@ -251,7 +333,13 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
251
333
|
*/
|
|
252
334
|
addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) => MaybePromise<unknown>): this;
|
|
253
335
|
addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) => MaybePromise<unknown>): this;
|
|
254
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Hands the instance to a function that registers on it (routes, hooks,
|
|
338
|
+
* actions) and continues the chain with what it returns. Endpoints are
|
|
339
|
+
* not registered this way: they are values, registered by
|
|
340
|
+
* registerApiGroups(), so a plugin adds nothing to the contract.
|
|
341
|
+
*/
|
|
342
|
+
use(plugin: (lambder: this) => unknown): this;
|
|
255
343
|
/**
|
|
256
344
|
* A session controller for a context: what creates, rotates, refreshes
|
|
257
345
|
* and ends sessions. An API call presents its posted CSRF token; a route
|
|
@@ -259,9 +347,9 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
259
347
|
* carries one as `ctx.sessionController`; this is for a context it did
|
|
260
348
|
* not render, such as one createContext() built from an event on its own.
|
|
261
349
|
*/
|
|
262
|
-
getSessionController(ctx: LambderRenderContext | LambderSessionRenderContext<any,
|
|
350
|
+
getSessionController(ctx: LambderRenderContext | LambderSessionRenderContext<any, TApp["session"]>): LambderSessionController<TApp["session"]>;
|
|
263
351
|
/** The session manager, for code that works on sessions outside a request (maintenance, tests). */
|
|
264
|
-
getSessionManager(): LambderSessionManager<
|
|
352
|
+
getSessionManager(): LambderSessionManager<TApp["session"]>;
|
|
265
353
|
/**
|
|
266
354
|
* The backend swap `lambder/testing` performs: the stores given go under
|
|
267
355
|
* this instance in place, so every handler and guard that closed over it
|
|
@@ -316,7 +404,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
316
404
|
* same object. Every table is sorted by name, so the module diffs by
|
|
317
405
|
* endpoint and never moves when registrations are reordered.
|
|
318
406
|
*/
|
|
319
|
-
apiOptionEntries(): LambderApiOptionEntries
|
|
407
|
+
apiOptionEntries(): Promise<LambderApiOptionEntries>;
|
|
320
408
|
getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder;
|
|
321
409
|
private getResolver;
|
|
322
410
|
getHandler(): LambderHandler;
|
|
@@ -341,6 +429,12 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
341
429
|
*/
|
|
342
430
|
private runBeforeRenderHooks;
|
|
343
431
|
private handleNoMatchedAction;
|
|
432
|
+
/**
|
|
433
|
+
* What answers a request nothing matched, its beforeRender hooks run:
|
|
434
|
+
* the fallback hooks, then the API fallback for an API call, and
|
|
435
|
+
* otherwise the public files, the shell and the route fallback in turn.
|
|
436
|
+
*/
|
|
437
|
+
private answerUnmatched;
|
|
344
438
|
/**
|
|
345
439
|
* True for the OPTIONS request the CORS layer answers by itself. Asked
|
|
346
440
|
* twice: once to build the 204, once at the end of render() to pick which
|
|
@@ -446,7 +540,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
446
540
|
/**
|
|
447
541
|
* A thrown LambderApiRefusal (from a hook, say) as the structured API
|
|
448
542
|
* envelope: the core's one mapping, after the same check the pipeline
|
|
449
|
-
* applies, against the endpoint the call names
|
|
543
|
+
* applies, against the endpoint the call names, its lazy group loaded if
|
|
544
|
+
* the refusal came before the call reached it. A name no API is
|
|
450
545
|
* registered under declares no code, so only an uncoded or a framework
|
|
451
546
|
* refusal goes out for it.
|
|
452
547
|
*/
|
|
@@ -456,11 +551,12 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
456
551
|
* The canonical way to create an instance: fix the session data type first,
|
|
457
552
|
* then create with the full configuration in one declaration. The policy,
|
|
458
553
|
* guard and idempotency types are inferred from the options, so the instance
|
|
459
|
-
* is born fully typed and
|
|
460
|
-
*
|
|
554
|
+
* is born fully typed, and the endpoints declared with its defineApi are
|
|
555
|
+
* typed against it. There are no ordering rules, and nothing can be
|
|
556
|
+
* half-configured.
|
|
461
557
|
*
|
|
462
558
|
* ```typescript
|
|
463
|
-
* // app.ts (imports no api modules, so modules can import
|
|
559
|
+
* // app.ts (imports no api modules, so modules can import from it)
|
|
464
560
|
* export const lambderApp = initLambder<SessionData>().create({
|
|
465
561
|
* apiPath: "/api",
|
|
466
562
|
* session: { store: new LambderDdbSessionStore({ tableName: "app-session", region: "us-east-1" }), sessionSalt: "..." },
|
|
@@ -468,13 +564,15 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
468
564
|
* guards,
|
|
469
565
|
* idempotency: { store },
|
|
470
566
|
* });
|
|
471
|
-
* export
|
|
567
|
+
* export const { defineApi, defineApiGroup, lazyApiGroup } = lambderApp;
|
|
472
568
|
*
|
|
473
569
|
* // orders.ts
|
|
474
|
-
* export const
|
|
570
|
+
* export const orderApis = defineApiGroup("orders", {
|
|
571
|
+
* place: defineApi({ input, output, guards: "signedIn" }, async (ctx) => ...),
|
|
572
|
+
* });
|
|
475
573
|
*
|
|
476
574
|
* // index.ts: registration only
|
|
477
|
-
* const lambder = lambderApp.
|
|
575
|
+
* const lambder = lambderApp.registerApiGroups(orderApis).addHook(...);
|
|
478
576
|
* export const handler = lambder.getHandler();
|
|
479
577
|
* ```
|
|
480
578
|
*
|
|
@@ -518,9 +616,18 @@ export declare const initLambder: <TSessionData = any>() => {
|
|
|
518
616
|
* endpoint's codes alone.
|
|
519
617
|
*/
|
|
520
618
|
refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TRefusals, keyof TRefusals & string>, TRequireCodes>;
|
|
521
|
-
create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "
|
|
522
|
-
|
|
523
|
-
|
|
619
|
+
create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, TRefusals>): Lambder<{
|
|
620
|
+
session: TSessionData;
|
|
621
|
+
policies: TOptions["rateLimits"] extends {
|
|
622
|
+
policies: infer TPolicies;
|
|
623
|
+
} ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {};
|
|
624
|
+
guards: TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {};
|
|
625
|
+
idempotency: TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false;
|
|
626
|
+
guardsRequired: [LambderGivenOption<TOptions, "requireApiGuards">] extends [false | undefined] ? false : true;
|
|
627
|
+
sessions: [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true;
|
|
628
|
+
refusals: TRefusals;
|
|
629
|
+
refusalCodesRequired: TRequireCodes;
|
|
630
|
+
}, {}>;
|
|
524
631
|
guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, TRefusals, TRequireCodes>;
|
|
525
632
|
rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
|
|
526
633
|
};
|
|
@@ -544,9 +651,18 @@ export declare const initLambder: <TSessionData = any>() => {
|
|
|
544
651
|
code?: undefined;
|
|
545
652
|
data?: undefined;
|
|
546
653
|
}) | undefined) => never;
|
|
547
|
-
create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "
|
|
548
|
-
|
|
549
|
-
|
|
654
|
+
create<const TOptions extends LambderInitCreateOptions<TSessionData>>(options: TOptions & Record<Exclude<keyof TOptions, "session" | "guards" | "idempotency" | "apiVersion" | "cors" | "apiPath" | "apiSignatures" | "trustedClientIpHeaders" | "trustedHostHeaders" | "files" | "etag" | "rateLimits" | "minApiVersion" | "compression" | "maxResponseBytes" | "maxRequestPayloadBytes" | "requireApiGuards" | "crashes">, never> & LambderNestedOptionChecks<TSessionData, TOptions, {}>): Lambder<{
|
|
655
|
+
session: TSessionData;
|
|
656
|
+
policies: TOptions["rateLimits"] extends {
|
|
657
|
+
policies: infer TPolicies;
|
|
658
|
+
} ? LambderMergedNamedMaps<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {} : {};
|
|
659
|
+
guards: TOptions["guards"] extends LambderNamedMapsOption<Record<string, LambderApiGuard<any, any, any>>> ? LambderGuardMetaMap<LambderMergedNamedMaps<TOptions["guards"]>> : {};
|
|
660
|
+
idempotency: TOptions["idempotency"] extends LambderApiIdempotencyConfig ? true : false;
|
|
661
|
+
guardsRequired: [LambderGivenOption<TOptions, "requireApiGuards">] extends [false | undefined] ? false : true;
|
|
662
|
+
sessions: [LambderGivenOption<TOptions, "session">] extends [undefined] ? false : true;
|
|
663
|
+
refusals: {};
|
|
664
|
+
refusalCodesRequired: false;
|
|
665
|
+
}, {}>;
|
|
550
666
|
guard: import("../api/LambderApiGuards.js").LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, {}, false>;
|
|
551
667
|
rateLimitKey: import("../api/LambderApiRateLimits.js").LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
|
|
552
668
|
};
|