lambder 15.1.1 → 16.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 +92 -0
- package/dist/api/LambderApiDeclarations.d.ts +21 -5
- package/dist/api/LambderApiDeclarations.js +21 -3
- package/dist/api/LambderApiGuards.d.ts +23 -3
- package/dist/api/LambderApiGuards.js +50 -25
- package/dist/api/LambderApiModules.d.ts +50 -0
- package/dist/api/LambderApiModules.js +36 -0
- package/dist/api/LambderApiPipeline.d.ts +27 -4
- package/dist/api/LambderApiPipeline.js +71 -5
- package/dist/api/LambderApiRateLimits.d.ts +35 -4
- package/dist/api/LambderApiRateLimits.js +74 -35
- package/dist/core/Lambder.d.ts +62 -87
- package/dist/core/Lambder.js +105 -57
- package/dist/core/LambderApiBuilders.d.ts +105 -0
- package/dist/core/LambderApiBuilders.js +12 -0
- package/dist/core/LambderApiModule.d.ts +106 -0
- package/dist/core/LambderApiModule.js +45 -0
- package/dist/core/LambderCreateOptions.d.ts +20 -16
- package/dist/index.d.ts +10 -5
- package/dist/index.js +2 -0
- package/dist/mock/LambderMockApiModule.d.ts +70 -0
- package/dist/mock/LambderMockApiModule.js +34 -0
- package/dist/mock/LambderMockApp.d.ts +53 -8
- package/dist/mock/LambderMockApp.js +104 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +18 -18
- package/dist/mock/LambderMockTypes.d.ts +46 -26
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +2 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/package.json +1 -1
package/dist/core/Lambder.js
CHANGED
|
@@ -13,7 +13,7 @@ import { LambderFiles } from "./LambderFiles.js";
|
|
|
13
13
|
import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
14
14
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
15
15
|
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
16
|
-
import { declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
16
|
+
import { assertRefusalCodesDeclared, declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
17
17
|
import { refuse } from "../shared/wire/LambderApiRefusal.js";
|
|
18
18
|
import { apiSignatureOf } from "../api/LambderApiSignature.js";
|
|
19
19
|
import { apiNameKeyOf } from "../shared/wire/LambderApiSignatureMap.js";
|
|
@@ -22,7 +22,10 @@ import { apiNotFoundAnswer, envelopeAnswer, sessionExpiredAnswer, successEnvelop
|
|
|
22
22
|
import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
|
|
23
23
|
import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
|
|
24
24
|
import { toGuardEntries } from "../api/LambderApiGuards.js";
|
|
25
|
-
import {
|
|
25
|
+
import { apiModuleDeclarationsOf } from "../api/LambderApiModules.js";
|
|
26
|
+
import { apiModuleUsesOf, isRegistrableApiGroup, } from "../api/LambderApiDeclarations.js";
|
|
27
|
+
import { apiBuildersFor, } from "./LambderApiBuilders.js";
|
|
28
|
+
import { LambderApiModule, } from "./LambderApiModule.js";
|
|
26
29
|
import { splitApiName } from "../shared/wire/LambderApiNames.js";
|
|
27
30
|
import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
|
|
28
31
|
import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
|
|
@@ -36,6 +39,8 @@ import { policyBuildersFor } from "./LambderPolicyBuilders.js";
|
|
|
36
39
|
import { assertCreateOptions, mergeNamedMaps, } from "./LambderCreateOptions.js";
|
|
37
40
|
/** The refusals of a name no API is registered under: no code, and none required. */
|
|
38
41
|
const NO_DECLARED_REFUSALS = { codes: new Map(), codeRequired: false };
|
|
42
|
+
/** The instance's own declaration builders: no module, and nothing of any instance, so every instance hands out the same three functions, each typed on its own app types. */
|
|
43
|
+
const INSTANCE_BUILDERS = apiBuildersFor(null);
|
|
39
44
|
/** Longest endpoint name a call summary carries: a page built before paths posts the name in its body, and a line is no place for a long one. */
|
|
40
45
|
const MAX_SUMMARY_NAME_CHARS = 200;
|
|
41
46
|
/** A request id as the parent header may carry one: what Lambda's own ids look like, and no longer than any of them. */
|
|
@@ -128,9 +133,14 @@ export default class Lambder {
|
|
|
128
133
|
apiGroupNames = new Set();
|
|
129
134
|
/** The lazy groups not loaded yet, each with its load in progress, if one is. */
|
|
130
135
|
lazyApiGroups = new Map();
|
|
131
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* The guards given at creation merged into one map, and every registered
|
|
138
|
+
* module's beside them, kept for registration (a guard's session and
|
|
139
|
+
* refusal codes), apiSignatures() (a guard's schema is part of the
|
|
140
|
+
* signature of every endpoint declaring it) and apiOptionEntries().
|
|
141
|
+
*/
|
|
132
142
|
guards;
|
|
133
|
-
/** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
143
|
+
/** The rate-limit policies given at creation, and every registered module's beside them, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
134
144
|
rateLimitPolicies;
|
|
135
145
|
/** The refusal vocabulary given at creation: what each API's and each guard's refusal codes resolve against. Null without the option. */
|
|
136
146
|
refusalVocabulary;
|
|
@@ -170,7 +180,10 @@ export default class Lambder {
|
|
|
170
180
|
const options = {
|
|
171
181
|
...given,
|
|
172
182
|
guards: given.guards && mergeNamedMaps(given.guards, "guard"),
|
|
173
|
-
rateLimits: given.rateLimits && {
|
|
183
|
+
rateLimits: given.rateLimits && {
|
|
184
|
+
...given.rateLimits,
|
|
185
|
+
policies: given.rateLimits.policies === undefined ? undefined : mergeNamedMaps(given.rateLimits.policies, "rate-limit policy"),
|
|
186
|
+
},
|
|
174
187
|
};
|
|
175
188
|
assertCreateOptions(options);
|
|
176
189
|
this.files = options.files ? new LambderFiles(options.files) : null;
|
|
@@ -189,8 +202,8 @@ export default class Lambder {
|
|
|
189
202
|
this.corsConfig = options.cors === true ? {} : options.cors;
|
|
190
203
|
}
|
|
191
204
|
const session = options.session;
|
|
192
|
-
this.guards = options.guards;
|
|
193
|
-
this.rateLimitPolicies = options.rateLimits?.policies;
|
|
205
|
+
this.guards = { ...options.guards };
|
|
206
|
+
this.rateLimitPolicies = { ...options.rateLimits?.policies };
|
|
194
207
|
this.refusalVocabulary = readRefusalVocabulary(options.refusals);
|
|
195
208
|
this.pipeline = new LambderApiPipeline({
|
|
196
209
|
apiVersion: this.apiVersion,
|
|
@@ -341,46 +354,56 @@ export default class Lambder {
|
|
|
341
354
|
// Declared as values, gathered into named groups, registered in one call.
|
|
342
355
|
// =====================================================================
|
|
343
356
|
/**
|
|
344
|
-
* Declares one endpoint, typed on this instance's types
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
357
|
+
* Declares one endpoint, typed on this instance's types (see
|
|
358
|
+
* LambderDefineApi). Nothing is registered until the declaration is put
|
|
359
|
+
* in a group (defineApiGroup) and the group is registered
|
|
360
|
+
* (registerApiGroups).
|
|
348
361
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* An arrow property rather than a method, so an app can hand it out
|
|
356
|
-
* detached: `export const { defineApi, defineApiGroup } = lambderApp;`.
|
|
362
|
+
* An arrow property rather than a method, as are the two below, so an
|
|
363
|
+
* app can hand them out detached; handed out under the instance's name
|
|
364
|
+
* (`export const defineApi: AppLambder["defineApi"] = lambderApp.defineApi`),
|
|
365
|
+
* a declaration file names each by the instance's type rather than
|
|
366
|
+
* writing the app's types out again in each.
|
|
357
367
|
*/
|
|
358
|
-
defineApi =
|
|
359
|
-
/**
|
|
360
|
-
|
|
368
|
+
defineApi = INSTANCE_BUILDERS.defineApi;
|
|
369
|
+
/** Gathers endpoints into a named group (see LambderDefineApiGroup). */
|
|
370
|
+
defineApiGroup = INSTANCE_BUILDERS.defineApiGroup;
|
|
371
|
+
/** A group loaded on the first call to one of its endpoints (see LambderDefineLazyApiGroup). */
|
|
372
|
+
lazyApiGroup = INSTANCE_BUILDERS.lazyApiGroup;
|
|
361
373
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
374
|
+
* Defines a module: guards and rate-limit policies a part of the app
|
|
375
|
+
* declares for its own endpoints, beside the ones this instance was
|
|
376
|
+
* created with. Returns the module's builders, whose endpoints may name
|
|
377
|
+
* the instance's guards and policies and the module's own, and nothing
|
|
378
|
+
* else; their groups are registered with registerApiGroups() as any
|
|
379
|
+
* are, and their registration adds the module's guards and policies to
|
|
380
|
+
* the instance that registers them.
|
|
367
381
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
* A
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* checks and loading checks again. `loadApiGroups()` loads every group,
|
|
381
|
-
* for a build step or a boot check that has to see them all.
|
|
382
|
+
* ```typescript
|
|
383
|
+
* export const giftCardModule = storeApp.defineApiModule({
|
|
384
|
+
* guards: { cardHolder },
|
|
385
|
+
* rateLimitPolicies: { redeemPerCard: { perHour: 5, per: "session" } },
|
|
386
|
+
* });
|
|
387
|
+
* ```
|
|
388
|
+
*
|
|
389
|
+
* A name the instance declares is refused here, at compile time and at
|
|
390
|
+
* the call; a name two modules declare is refused when the second is
|
|
391
|
+
* registered, as create() refuses a name two maps declare. A module's
|
|
392
|
+
* rate-limit policies count on the instance's limiter, so they need the
|
|
393
|
+
* rateLimits option at creation, whose own policies may then be left out.
|
|
382
394
|
*/
|
|
383
|
-
|
|
395
|
+
defineApiModule(options) {
|
|
396
|
+
const given = options;
|
|
397
|
+
const declarations = apiModuleDeclarationsOf(given, "Lambder");
|
|
398
|
+
for (const [name, guard] of Object.entries(declarations.guards)) {
|
|
399
|
+
assertRefusalCodesDeclared(`guard "${name}"`, (guard?.refusals ?? []), this.refusalVocabulary);
|
|
400
|
+
}
|
|
401
|
+
// Checked against this instance now, so a name it declares fails
|
|
402
|
+
// where the module is defined; registration checks again against
|
|
403
|
+
// whichever instance takes the module, and against the other modules.
|
|
404
|
+
this.pipeline.modulesToAdd([{ module: declarations, usedBy: "being defined" }]);
|
|
405
|
+
return new LambderApiModule(declarations);
|
|
406
|
+
}
|
|
384
407
|
/**
|
|
385
408
|
* Registers groups of endpoints, as one contract: `typeof
|
|
386
409
|
* lambder.ApiContract` then maps every `group.action` to its entry, one
|
|
@@ -411,7 +434,6 @@ export default class Lambder {
|
|
|
411
434
|
// caller that catches the error and fixes the declaration is told the
|
|
412
435
|
// real problem on its next try.
|
|
413
436
|
const names = new Set();
|
|
414
|
-
const prepared = [];
|
|
415
437
|
for (const group of groups) {
|
|
416
438
|
if (!isRegistrableApiGroup(group)) {
|
|
417
439
|
throw new Error("Lambder: registerApiGroups takes groups built by defineApiGroup() or lazyApiGroup().");
|
|
@@ -420,10 +442,21 @@ export default class Lambder {
|
|
|
420
442
|
throw new Error(`Lambder: group "${group.name}" is registered twice. A group is one namespace, so its endpoints belong in one defineApiGroup().`);
|
|
421
443
|
}
|
|
422
444
|
names.add(group.name);
|
|
445
|
+
}
|
|
446
|
+
// The modules the groups bring, a lazy group's at once: checked
|
|
447
|
+
// against the instance and each other here, taken only after every
|
|
448
|
+
// endpoint passed against them.
|
|
449
|
+
const registrable = groups;
|
|
450
|
+
const added = instance.pipeline.modulesToAdd(registrable.flatMap(apiModuleUsesOf));
|
|
451
|
+
const prepared = [];
|
|
452
|
+
for (const group of registrable) {
|
|
423
453
|
if (group.kind === "lambderApiGroup")
|
|
424
|
-
prepared.push(...instance.prepareApiGroup(group));
|
|
454
|
+
prepared.push(...instance.prepareApiGroup(group, added));
|
|
425
455
|
}
|
|
426
|
-
|
|
456
|
+
instance.pipeline.addModules(added);
|
|
457
|
+
instance.guards = { ...instance.guards, ...added.guards };
|
|
458
|
+
instance.rateLimitPolicies = { ...instance.rateLimitPolicies, ...added.rateLimitPolicies };
|
|
459
|
+
for (const group of registrable) {
|
|
427
460
|
instance.apiGroupNames.add(group.name);
|
|
428
461
|
if (group.kind === "lambderLazyApiGroup")
|
|
429
462
|
instance.lazyApiGroups.set(group.name, { group, loading: null });
|
|
@@ -478,9 +511,14 @@ export default class Lambder {
|
|
|
478
511
|
await this.loadLazyApiGroup(group);
|
|
479
512
|
return this.apiDefinitions.get(apiName);
|
|
480
513
|
}
|
|
481
|
-
/**
|
|
482
|
-
|
|
483
|
-
|
|
514
|
+
/**
|
|
515
|
+
* The endpoints of one group, each checked as `group.action` against the
|
|
516
|
+
* instance's guards and policies and those `adding` holds, of the modules
|
|
517
|
+
* the same registration adds; nothing is registered until they are
|
|
518
|
+
* committed.
|
|
519
|
+
*/
|
|
520
|
+
prepareApiGroup(group, adding) {
|
|
521
|
+
return Object.entries(group.apis).map(([action, declaration]) => this.prepareApi(`${group.name}.${action}`, declaration.options, declaration.handler, adding));
|
|
484
522
|
}
|
|
485
523
|
/**
|
|
486
524
|
* A lazy group, loaded once: its import, checked to be the group it was
|
|
@@ -501,6 +539,16 @@ export default class Lambder {
|
|
|
501
539
|
if (loaded.name !== name) {
|
|
502
540
|
throw new Error(`Lambder: lazy group "${name}" loaded the group "${loaded.name}". The loader must resolve to the group of the name it was registered under.`);
|
|
503
541
|
}
|
|
542
|
+
// A module's guards and policies are added when its group is
|
|
543
|
+
// registered, at startup, so a collision fails the boot and a
|
|
544
|
+
// route charging one of its policies finds it from the first
|
|
545
|
+
// request; one first met here would do neither.
|
|
546
|
+
for (const { module } of apiModuleUsesOf(loaded)) {
|
|
547
|
+
if (!this.pipeline.hasModule(module)) {
|
|
548
|
+
throw new Error(`Lambder: lazy group "${name}" loaded endpoints a module's builders declared, and was registered with a lazyApiGroup that does not carry that module. ` +
|
|
549
|
+
`Register it with the module's own lazyApiGroup, so its guards and rate-limit policies are added at startup.`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
504
552
|
for (const api of this.prepareApiGroup(loaded))
|
|
505
553
|
this.commitApi(api);
|
|
506
554
|
this.lazyApiGroups.delete(name);
|
|
@@ -517,9 +565,12 @@ export default class Lambder {
|
|
|
517
565
|
* digests, beside its handler. Nothing is recorded here (commitApi does
|
|
518
566
|
* that), so a refusal anywhere in a registration leaves nothing behind.
|
|
519
567
|
*/
|
|
520
|
-
prepareApi(name, schema, handler) {
|
|
568
|
+
prepareApi(name, schema, handler, adding) {
|
|
521
569
|
const guardEntries = toGuardEntries(schema.guards);
|
|
522
|
-
const
|
|
570
|
+
const guardOf = (guard) => Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]
|
|
571
|
+
: adding && Object.prototype.hasOwnProperty.call(adding.guards, guard) ? adding.guards[guard]
|
|
572
|
+
: undefined;
|
|
573
|
+
const guardNeedsSession = (guard) => guardOf(guard)?.session === true;
|
|
523
574
|
const mode = guardEntries.some(({ name: guard }) => guardNeedsSession(guard)) ? "session" : "public";
|
|
524
575
|
if (mode === "session" && !this.pipeline.hasSessions) {
|
|
525
576
|
throw new Error(`Lambder: a guard of API "${name}" needs a session, and the instance was created without the session option.`);
|
|
@@ -534,13 +585,10 @@ export default class Lambder {
|
|
|
534
585
|
if (schema.refusals !== undefined && ownRefusals.length === 0) {
|
|
535
586
|
throw new Error(`Lambder: API "${name}" declares an empty refusals option, which declares no code. Name the codes it refuses with, or omit the option entirely.`);
|
|
536
587
|
}
|
|
537
|
-
const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, guardEntries.map(({ name: guard }) => ({
|
|
538
|
-
guard,
|
|
539
|
-
codes: (this.guards && Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]?.refusals : undefined) ?? [],
|
|
540
|
-
})));
|
|
588
|
+
const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, guardEntries.map(({ name: guard }) => ({ guard, codes: guardOf(guard)?.refusals ?? [] })));
|
|
541
589
|
const refusals = { codes: allowedCodes, codeRequired: this.requireRefusalCodes };
|
|
542
590
|
const definition = { name, mode, guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input, output: schema.output, refusals };
|
|
543
|
-
this.pipeline.assertRegistration(definition);
|
|
591
|
+
this.pipeline.assertRegistration(definition, adding);
|
|
544
592
|
return { definition, refusalsOption: schema.refusals, output: schema.output, compress: schema.compress ?? "auto", handler };
|
|
545
593
|
}
|
|
546
594
|
/** Records a checked endpoint: its definition, its refusals option as written, and what answers it. */
|
|
@@ -703,7 +751,7 @@ export default class Lambder {
|
|
|
703
751
|
apis[name] = entry;
|
|
704
752
|
}
|
|
705
753
|
const rateLimitPolicies = {};
|
|
706
|
-
for (const name of Object.keys(this.rateLimitPolicies
|
|
754
|
+
for (const name of Object.keys(this.rateLimitPolicies).sort()) {
|
|
707
755
|
const { per, budget, chargeAt, refusal, ...windows } = this.rateLimitPolicies[name];
|
|
708
756
|
const entry = {};
|
|
709
757
|
for (const [window, limit] of Object.entries(windows)) {
|
|
@@ -723,7 +771,7 @@ export default class Lambder {
|
|
|
723
771
|
rateLimitPolicies[name] = entry;
|
|
724
772
|
}
|
|
725
773
|
const guards = {};
|
|
726
|
-
for (const name of Object.keys(this.guards
|
|
774
|
+
for (const name of Object.keys(this.guards).sort()) {
|
|
727
775
|
const guard = this.guards[name];
|
|
728
776
|
guards[name] = {
|
|
729
777
|
input: guard.apiInput ? "apiInput" : guard.guardInput ? "guardInput" : "none",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
3
|
+
import type { LambderObjectOutputCheck, LambderPayloadSliceCheck, LambderRequirableGuardsField } from "./LambderCreateOptions.js";
|
|
4
|
+
import type { LambderReadonlyDeep, LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
5
|
+
import type { LambderDeclaredRefuse } from "../shared/wire/LambderApiRefusal.js";
|
|
6
|
+
import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
|
|
7
|
+
import type { LambderContractEntry, LambderJsonOf } from "../shared/wire/LambderApiContract.js";
|
|
8
|
+
import type { LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusalNamesOf, LambderGuardsOption } from "../api/LambderApiGuards.js";
|
|
9
|
+
import type { LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
|
|
10
|
+
import type { LambderHandlerRefusalsOf, LambderRefusalNamesIn, LambderRefusalsOption, LambderWireRefusalsOf } from "../api/LambderApiRefusals.js";
|
|
11
|
+
import type { LambderApiModuleDeclarations } from "../api/LambderApiModules.js";
|
|
12
|
+
import { type LambderActionNamesCheck, type LambderApiDeclaration, type LambderApiDeclarations, type LambderApiGroup, type LambderApiModeOf, type LambderAppTypes, type LambderGroupNameCheck, type LambderGroupPartsCheck, type LambderLazyApiGroup, type LambderMergedParts, type LambderPublicSessionPolicyCheck, type LambderSessionModeCheck } from "../api/LambderApiDeclarations.js";
|
|
13
|
+
/** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
|
|
14
|
+
export type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
|
|
15
|
+
/**
|
|
16
|
+
* What an endpoint's handler is handed: the session context when one of its
|
|
17
|
+
* guards needs a session (so ctx.session is typed present), the plain one
|
|
18
|
+
* otherwise, with the payload, the guards' data and a refuse() typed to the
|
|
19
|
+
* codes the endpoint may refuse with.
|
|
20
|
+
*/
|
|
21
|
+
export 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"]>) & {
|
|
22
|
+
refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Declares one endpoint, typed on one set of app types: its input and output
|
|
26
|
+
* schemas, its guards, rate limits, idempotency and refusals, and its
|
|
27
|
+
* handler. Nothing is registered until the declaration is put in a group
|
|
28
|
+
* (defineApiGroup) and the group is registered (registerApiGroups).
|
|
29
|
+
*
|
|
30
|
+
* The guards say who may call it, and so its mode: an endpoint whose guards
|
|
31
|
+
* include one that needs a session (`session: true`) is a session endpoint,
|
|
32
|
+
* its session read before the guards run and a call without one answered
|
|
33
|
+
* sessionExpired, and its handler's ctx.session is typed present. Any other
|
|
34
|
+
* endpoint is public.
|
|
35
|
+
*/
|
|
36
|
+
export type LambderDefineApi<TApp extends LambderAppTypes> = <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: {
|
|
37
|
+
input: TInput;
|
|
38
|
+
output: TOutput;
|
|
39
|
+
} & {
|
|
40
|
+
/** 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. */
|
|
41
|
+
rateLimit?: TRateOpt;
|
|
42
|
+
/** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
|
|
43
|
+
idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
|
|
44
|
+
/**
|
|
45
|
+
* The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
|
|
46
|
+
* non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
|
|
47
|
+
* its callers narrow on them, and a refusal with any other code is a crash rather than an answer.
|
|
48
|
+
*/
|
|
49
|
+
refusals?: TRefusalsOpt;
|
|
50
|
+
/**
|
|
51
|
+
* Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
|
|
52
|
+
* body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
|
|
53
|
+
* compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
|
|
54
|
+
* cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
|
|
55
|
+
* both ends. A transport setting of this server's, not part of the API's contract.
|
|
56
|
+
*/
|
|
57
|
+
compress?: boolean | "auto";
|
|
58
|
+
} & 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"]>,
|
|
59
|
+
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
60
|
+
handler: (ctx: LambderApiHandlerContext<TApp, z.infer<TInput>, TGuardsOpt, TRefusalsOpt>) => LambderMaybePromise<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>>>>;
|
|
61
|
+
/**
|
|
62
|
+
* Gathers endpoints into a named group: each is registered as `name.action`
|
|
63
|
+
* and called at `{apiPath}/{name}/{action}`, and every Lambder caller
|
|
64
|
+
* reaches it as `caller.name.action(input)`. A group's name and its actions
|
|
65
|
+
* are identifiers, and a group may not take a name a caller already answers
|
|
66
|
+
* for (see LAMBDER_RESERVED_GROUP_NAMES).
|
|
67
|
+
*
|
|
68
|
+
* A group declared across files takes each file's part:
|
|
69
|
+
* `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action two
|
|
70
|
+
* parts declare is refused, at compile time and at startup, rather than one
|
|
71
|
+
* silently replacing the other as an object spread would. Endpoints a
|
|
72
|
+
* module's builders declared may sit in any group: registering it adds
|
|
73
|
+
* their module.
|
|
74
|
+
*/
|
|
75
|
+
export type LambderDefineApiGroup = <const TName extends string, const TParts extends readonly [LambderApiDeclarations, ...LambderApiDeclarations[]]>(name: TName & LambderGroupNameCheck<TName>, ...parts: TParts & LambderGroupPartsCheck<TParts> & {
|
|
76
|
+
[TIndex in keyof TParts]: LambderActionNamesCheck<TParts[TIndex]>;
|
|
77
|
+
}) => LambderApiGroup<TName, LambderMergedParts<TParts>>;
|
|
78
|
+
/**
|
|
79
|
+
* A group loaded on the first call to one of its endpoints:
|
|
80
|
+
* `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
|
|
81
|
+
* A cold start then parses none of it, nor anything only it imports, until
|
|
82
|
+
* a request calls it; the contract is the loaded group's, read off its type.
|
|
83
|
+
* The group loaded must carry the same name, which the type checks and
|
|
84
|
+
* loading checks again. `loadApiGroups()` loads every group, for a build
|
|
85
|
+
* step or a boot check that has to see them all.
|
|
86
|
+
*
|
|
87
|
+
* A module's lazyApiGroup carries the module, so its guards and policies
|
|
88
|
+
* are added when the group is registered, at startup, rather than when it
|
|
89
|
+
* loads; a group whose endpoints a module's builders declared is registered
|
|
90
|
+
* lazily with that module's.
|
|
91
|
+
*/
|
|
92
|
+
export type LambderDefineLazyApiGroup = <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
|
|
93
|
+
/** The three builders, typed on one set of app types. */
|
|
94
|
+
export type LambderApiBuilders<TApp extends LambderAppTypes> = {
|
|
95
|
+
readonly defineApi: LambderDefineApi<TApp>;
|
|
96
|
+
readonly defineApiGroup: LambderDefineApiGroup;
|
|
97
|
+
readonly lazyApiGroup: LambderDefineLazyApiGroup;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The builders as values: the instance's own (module null), or a module's,
|
|
101
|
+
* whose endpoints and lazy groups carry the module for registration to add.
|
|
102
|
+
* Plain functions closing over nothing else, so an app may hand them out
|
|
103
|
+
* detached.
|
|
104
|
+
*/
|
|
105
|
+
export declare const apiBuildersFor: <TApp extends LambderAppTypes>(module: LambderApiModuleDeclarations | null) => LambderApiBuilders<TApp>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildApiGroup, buildLazyApiGroup, } from "../api/LambderApiDeclarations.js";
|
|
2
|
+
/**
|
|
3
|
+
* The builders as values: the instance's own (module null), or a module's,
|
|
4
|
+
* whose endpoints and lazy groups carry the module for registration to add.
|
|
5
|
+
* Plain functions closing over nothing else, so an app may hand them out
|
|
6
|
+
* detached.
|
|
7
|
+
*/
|
|
8
|
+
export const apiBuildersFor = (module) => ({
|
|
9
|
+
defineApi: ((options, handler) => module ? { kind: "lambderApi", options, handler, module } : { kind: "lambderApi", options, handler }),
|
|
10
|
+
defineApiGroup: ((name, ...parts) => buildApiGroup(name, parts)),
|
|
11
|
+
lazyApiGroup: ((name, load) => buildLazyApiGroup(name, load, module)),
|
|
12
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { LambderAppTypes, LambderPlainAppTypes } from "../api/LambderApiDeclarations.js";
|
|
2
|
+
import type { LambderApiModuleDeclarations } from "../api/LambderApiModules.js";
|
|
3
|
+
import type { LambderGuardMetaMap } from "../api/LambderApiGuards.js";
|
|
4
|
+
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
5
|
+
import type { LambderMergedNamedMaps, LambderNamedMapsOption } from "../shared/util/LambderNamedMaps.js";
|
|
6
|
+
import { type LambderDefineApi, type LambderDefineApiGroup, type LambderDefineLazyApiGroup } from "./LambderApiBuilders.js";
|
|
7
|
+
import type { LambderGivenOption, LambderGuardChecks, LambderGuardsMap, LambderRateLimitPoliciesMap, LambderRateLimitPolicyChecks } from "./LambderCreateOptions.js";
|
|
8
|
+
/**
|
|
9
|
+
* What defineApiModule() takes: the guards and the rate-limit policies a
|
|
10
|
+
* module declares for its own endpoints, each one map or a list of maps, as
|
|
11
|
+
* create() takes its own. The guards are pinned to the app's session type,
|
|
12
|
+
* as create()'s are.
|
|
13
|
+
*/
|
|
14
|
+
export type LambderApiModuleOptions<TSessionData = any> = {
|
|
15
|
+
/** The module's guards, which its endpoints name beside the instance's: one map, or a list of maps merged in order. */
|
|
16
|
+
guards?: LambderNamedMapsOption<LambderGuardsMap<TSessionData>>;
|
|
17
|
+
/** The module's rate-limit policies, counted on the instance's limiter, which its endpoints name beside the instance's: one map, or a list of maps merged in order. */
|
|
18
|
+
rateLimitPolicies?: LambderNamedMapsOption<LambderRateLimitPoliciesMap>;
|
|
19
|
+
};
|
|
20
|
+
/** Every name a map, or each map of a list, declares. */
|
|
21
|
+
type LambderNamesDeclaredBy<TMaps> = TMaps extends readonly unknown[] ? {
|
|
22
|
+
[TIndex in keyof TMaps]: keyof TMaps[TIndex];
|
|
23
|
+
}[number] : keyof TMaps;
|
|
24
|
+
/** A module's map held to declaring names of its own: one the instance declares is a compile error, and the property name is the message. */
|
|
25
|
+
type LambderModuleNamesCheck<TMaps, TInstanceNames> = [
|
|
26
|
+
Extract<LambderNamesDeclaredBy<TMaps>, TInstanceNames>
|
|
27
|
+
] extends [never] ? unknown : {
|
|
28
|
+
readonly "lambder: the instance declares these names already; a module declares names of its own": Extract<LambderNamesDeclaredBy<TMaps>, TInstanceNames>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* defineApiModule()'s options held to what create() holds its own to, one
|
|
32
|
+
* level down (each guard and each policy checked for surplus keys, a guard's
|
|
33
|
+
* refusal codes held to the vocabulary, a name two maps of a list declare
|
|
34
|
+
* refused), and to declaring no name the instance declares.
|
|
35
|
+
*/
|
|
36
|
+
export type LambderApiModuleOptionChecks<TApp extends LambderAppTypes, TOptions extends LambderApiModuleOptions<TApp["session"]>> = {
|
|
37
|
+
guards?: LambderGuardChecks<TApp["session"], NonNullable<LambderGivenOption<TOptions, "guards">>, TApp["refusals"]> & LambderModuleNamesCheck<NonNullable<LambderGivenOption<TOptions, "guards">>, keyof TApp["guards"]>;
|
|
38
|
+
rateLimitPolicies?: LambderRateLimitPolicyChecks<NonNullable<LambderGivenOption<TOptions, "rateLimitPolicies">>> & LambderModuleNamesCheck<NonNullable<LambderGivenOption<TOptions, "rateLimitPolicies">>, keyof TApp["policies"]>;
|
|
39
|
+
} & LambderModuleDeclaresSomething<TOptions>;
|
|
40
|
+
/**
|
|
41
|
+
* A module declaring neither guards nor policies, refused as an empty guards
|
|
42
|
+
* option is: its builders would be the instance's own. The property name is
|
|
43
|
+
* the message.
|
|
44
|
+
*/
|
|
45
|
+
type LambderModuleDeclaresSomething<TOptions> = [
|
|
46
|
+
LambderGivenOption<TOptions, "guards"> | LambderGivenOption<TOptions, "rateLimitPolicies">
|
|
47
|
+
] extends [undefined] ? {
|
|
48
|
+
readonly "lambder: a module declares guards or rate-limit policies of its own; one with neither declares its endpoints with the instance's builders": never;
|
|
49
|
+
} : unknown;
|
|
50
|
+
/** One map from what a module option was given: a list merged, a lone map as it is, nothing for an option left out. */
|
|
51
|
+
type LambderModuleMap<TOption> = [TOption] extends [undefined] ? {} : LambderMergedNamedMaps<TOption>;
|
|
52
|
+
/**
|
|
53
|
+
* The app types a module's builders are typed on: the instance's, with the
|
|
54
|
+
* module's guards and rate-limit policies beside the instance's own. Its
|
|
55
|
+
* endpoints name either, and nothing else.
|
|
56
|
+
*/
|
|
57
|
+
export type LambderModuleAppTypes<TApp extends LambderAppTypes, TGuards, TPolicies> = {
|
|
58
|
+
session: TApp["session"];
|
|
59
|
+
policies: TApp["policies"] & (LambderModuleMap<TPolicies> extends infer TMerged extends Record<string, LambderApiRateLimitPolicyConfig> ? TMerged : {});
|
|
60
|
+
guards: TApp["guards"] & LambderGuardMetaMap<LambderModuleMap<TGuards>>;
|
|
61
|
+
idempotency: TApp["idempotency"];
|
|
62
|
+
guardsRequired: TApp["guardsRequired"];
|
|
63
|
+
sessions: TApp["sessions"];
|
|
64
|
+
refusals: TApp["refusals"];
|
|
65
|
+
refusalCodesRequired: TApp["refusalCodesRequired"];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A part of an app with guards and rate-limit policies of its own, for its
|
|
69
|
+
* own endpoints: the declaration builders typed on its instance's types with
|
|
70
|
+
* the module's guards and policies beside the instance's. Made by an
|
|
71
|
+
* instance's defineApiModule(); its endpoints are registered on the instance
|
|
72
|
+
* as any are (registerApiGroups), and their registration adds the module's
|
|
73
|
+
* guards and policies to the instance, refusing a name the instance or
|
|
74
|
+
* another module declares.
|
|
75
|
+
*
|
|
76
|
+
* So an app's core creates its instance knowing only its own policies, and
|
|
77
|
+
* each module brings its own: nothing has to import every module's guards
|
|
78
|
+
* before the instance can exist. Hand the builders out under the module's
|
|
79
|
+
* name, as an instance's are:
|
|
80
|
+
*
|
|
81
|
+
* ```typescript
|
|
82
|
+
* export const giftCardModule = storeApp.defineApiModule({ guards: { cardHolder }, rateLimitPolicies: { redeemPerCard } });
|
|
83
|
+
* export type GiftCardModule = typeof giftCardModule;
|
|
84
|
+
* export const defineApi: GiftCardModule["defineApi"] = giftCardModule.defineApi;
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @typeParam TApp - @internal The module's app types (LambderModuleAppTypes): the instance's, widened by the module's guards and policies (do not pass manually)
|
|
88
|
+
*/
|
|
89
|
+
export declare class LambderApiModule<TApp extends LambderAppTypes = LambderPlainAppTypes> {
|
|
90
|
+
/**
|
|
91
|
+
* Type property for the module's app types, as an instance's AppTypes
|
|
92
|
+
* is: what LambderRenderContextOf<typeof module> reads, for a helper
|
|
93
|
+
* handed one of the module's handler contexts, its policies typed. It
|
|
94
|
+
* has no value.
|
|
95
|
+
*/
|
|
96
|
+
readonly AppTypes: TApp;
|
|
97
|
+
/** Declares one of the module's endpoints: its guards and rate limits may name the instance's and the module's own. */
|
|
98
|
+
readonly defineApi: LambderDefineApi<TApp>;
|
|
99
|
+
/** Gathers endpoints into a named group, as the instance's does. */
|
|
100
|
+
readonly defineApiGroup: LambderDefineApiGroup;
|
|
101
|
+
/** A group of the module's loaded on the first call to it, carrying the module, so its guards and policies are added when the group is registered. */
|
|
102
|
+
readonly lazyApiGroup: LambderDefineLazyApiGroup;
|
|
103
|
+
/** Called by an instance's defineApiModule(), with the declarations it checked; an app does not construct one. */
|
|
104
|
+
constructor(declarations: LambderApiModuleDeclarations);
|
|
105
|
+
}
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { apiBuildersFor, } from "./LambderApiBuilders.js";
|
|
2
|
+
/**
|
|
3
|
+
* A part of an app with guards and rate-limit policies of its own, for its
|
|
4
|
+
* own endpoints: the declaration builders typed on its instance's types with
|
|
5
|
+
* the module's guards and policies beside the instance's. Made by an
|
|
6
|
+
* instance's defineApiModule(); its endpoints are registered on the instance
|
|
7
|
+
* as any are (registerApiGroups), and their registration adds the module's
|
|
8
|
+
* guards and policies to the instance, refusing a name the instance or
|
|
9
|
+
* another module declares.
|
|
10
|
+
*
|
|
11
|
+
* So an app's core creates its instance knowing only its own policies, and
|
|
12
|
+
* each module brings its own: nothing has to import every module's guards
|
|
13
|
+
* before the instance can exist. Hand the builders out under the module's
|
|
14
|
+
* name, as an instance's are:
|
|
15
|
+
*
|
|
16
|
+
* ```typescript
|
|
17
|
+
* export const giftCardModule = storeApp.defineApiModule({ guards: { cardHolder }, rateLimitPolicies: { redeemPerCard } });
|
|
18
|
+
* export type GiftCardModule = typeof giftCardModule;
|
|
19
|
+
* export const defineApi: GiftCardModule["defineApi"] = giftCardModule.defineApi;
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @typeParam TApp - @internal The module's app types (LambderModuleAppTypes): the instance's, widened by the module's guards and policies (do not pass manually)
|
|
23
|
+
*/
|
|
24
|
+
export class LambderApiModule {
|
|
25
|
+
/**
|
|
26
|
+
* Type property for the module's app types, as an instance's AppTypes
|
|
27
|
+
* is: what LambderRenderContextOf<typeof module> reads, for a helper
|
|
28
|
+
* handed one of the module's handler contexts, its policies typed. It
|
|
29
|
+
* has no value.
|
|
30
|
+
*/
|
|
31
|
+
AppTypes;
|
|
32
|
+
/** Declares one of the module's endpoints: its guards and rate limits may name the instance's and the module's own. */
|
|
33
|
+
defineApi;
|
|
34
|
+
/** Gathers endpoints into a named group, as the instance's does. */
|
|
35
|
+
defineApiGroup;
|
|
36
|
+
/** A group of the module's loaded on the first call to it, carrying the module, so its guards and policies are added when the group is registered. */
|
|
37
|
+
lazyApiGroup;
|
|
38
|
+
/** Called by an instance's defineApiModule(), with the declarations it checked; an app does not construct one. */
|
|
39
|
+
constructor(declarations) {
|
|
40
|
+
const builders = apiBuildersFor(declarations);
|
|
41
|
+
this.defineApi = builders.defineApi;
|
|
42
|
+
this.defineApiGroup = builders.defineApiGroup;
|
|
43
|
+
this.lazyApiGroup = builders.lazyApiGroup;
|
|
44
|
+
}
|
|
45
|
+
}
|