lambder 12.0.1 → 14.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +7 -2
  3. package/dist/api/LambderApiAnswer.d.ts +7 -0
  4. package/dist/api/LambderApiEnvelope.js +4 -1
  5. package/dist/api/LambderApiRefusals.d.ts +22 -0
  6. package/dist/api/LambderApiRefusals.js +13 -0
  7. package/dist/build/generateApiFiles.d.ts +60 -0
  8. package/dist/build/generateApiFiles.js +85 -0
  9. package/dist/build/generatedTables.d.ts +1 -1
  10. package/dist/build/writeApiGuardParams.d.ts +11 -10
  11. package/dist/build/writeApiGuardParams.js +135 -31
  12. package/dist/build.d.ts +4 -1
  13. package/dist/build.js +3 -1
  14. package/dist/client.d.ts +2 -2
  15. package/dist/client.js +1 -1
  16. package/dist/core/Lambder.d.ts +32 -8
  17. package/dist/core/Lambder.js +135 -17
  18. package/dist/core/LambderCallSummary.d.ts +43 -0
  19. package/dist/core/LambderCallSummary.js +10 -0
  20. package/dist/core/LambderContext.d.ts +14 -1
  21. package/dist/core/LambderContext.js +19 -4
  22. package/dist/core/LambderCreateOptions.d.ts +50 -25
  23. package/dist/core/LambderCreateOptions.js +37 -18
  24. package/dist/core/LambderInvocationScope.d.ts +25 -0
  25. package/dist/core/LambderInvocationScope.js +21 -0
  26. package/dist/core/LambderResponse.d.ts +11 -0
  27. package/dist/core/LambderResponse.js +12 -0
  28. package/dist/core/LambderResponseBuilder.js +5 -1
  29. package/dist/index.d.ts +8 -6
  30. package/dist/index.js +2 -2
  31. package/dist/invoke/LambderLambdaEvent.d.ts +2 -0
  32. package/dist/invoke/LambderLambdaEvent.js +8 -2
  33. package/dist/mock/LambderMockApp.d.ts +8 -8
  34. package/dist/mock/LambderMockApp.js +14 -10
  35. package/dist/mock/LambderMockCreateOptions.d.ts +2 -3
  36. package/dist/session/LambderSessionManager.d.ts +46 -3
  37. package/dist/session/LambderSessionManager.js +62 -11
  38. package/dist/shared/LambderI18n.js +49 -4
  39. package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
  40. package/dist/shared/contracts/LambderRateLimiter.js +2 -2
  41. package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
  42. package/dist/shared/util/LambderInvocationScope.js +5 -0
  43. package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
  44. package/dist/shared/util/LambderNamedMaps.js +19 -0
  45. package/dist/shared/wire/LambderApiOptionEntries.d.ts +24 -0
  46. package/dist/shared/wire/LambderApiOptionEntries.js +13 -0
  47. package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
  48. package/dist/shared/wire/LambderCallOutcome.js +47 -0
  49. package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
  50. package/dist/shared/wire/LambderInvokeApiId.js +10 -0
  51. package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
  52. package/dist/stores/LambderDdbRateLimiter.js +64 -45
  53. package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
  54. package/dist/stores/LambderMemoryRateLimiter.js +20 -15
  55. package/dist/testing/LambderTestApp.d.ts +13 -4
  56. package/dist/testing/LambderTestApp.js +21 -5
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,125 @@ sit on its first published patch, and later patches list only what they changed.
9
9
  Releases up to 3.2.6 carry git tags; the ones after it were published without
10
10
  one, so versions are not cross-linked to tag comparisons here.
11
11
 
12
+ ## [14.0.1] - 2026-09-30
13
+
14
+ A major: `writeApiGuardParams` writes one export per API in place of one
15
+ table, so a browser carries only the guard parameters its screens import,
16
+ and none of the API names behind them.
17
+
18
+ ### Changed (breaking)
19
+
20
+ - **`writeApiGuardParams` writes one export per API** that declares the
21
+ guard, in place of the `guardParams` table: `orders.list` exports
22
+ `ordersListGuardParam`, holding the parameter as declared (`true` for the
23
+ guard named without one) and typed
24
+ `LambderApiGuardParam<"orders.list", "store", literal>`. A bundler keeps
25
+ only the exports a client imports, and none of them carries its API's name,
26
+ which sits in the export's type and doc comment alone, so a page no longer
27
+ ships the name of every API behind the guard and what each asks. The
28
+ module imports the tag type from `lambder/client`, as a type. Two APIs
29
+ whose names would export under one identifier fail the write, and so does
30
+ a `null` parameter, which the tag cannot carry; a guard no API declares
31
+ writes an empty module. A file still in the table form reads as one to
32
+ regenerate, on a check and on a write.
33
+ - To move: regenerate the file, and import the exports a screen gates on
34
+ in place of reading `guardParams[name]`. A function that takes any of a
35
+ guard's parameters reads their union off the module with a type-only
36
+ import (`(typeof params)[keyof typeof params]`, over
37
+ `import type * as params`). Code that needs every API's name, a test
38
+ walking them all, reads it from the `writeApiOptions` file.
39
+
40
+ ### Added
41
+
42
+ - **`LambderApiGuardParam`** (`lambder/client`, `lambder`): the type of each
43
+ export `writeApiGuardParams` writes, a guard's parameter tagged in types
44
+ alone with the API's name and the guard's, so a function typed to a
45
+ guard's parameters refuses a hand-written literal.
46
+ - **`apiGuardParamExportName`** (`lambder/client`, `lambder`): the identifier
47
+ `writeApiGuardParams` exports an API's parameter under, for code that reads
48
+ a generated module by API name, such as a test comparing it with the
49
+ server.
50
+
51
+ ## [13.0.1] - 2026-09-30
52
+
53
+ A major: session data is checked against a schema on every read, and
54
+ `dataRefresh` requires that schema. Beside it, one summary line per API call
55
+ by default, an origin proof for the trusted forwarding headers, refusal codes
56
+ declared per part of an app, a check that every translation keeps its
57
+ placeholders, one round trip for a rate limit's windows, and every generated
58
+ file in one call.
59
+
60
+ ### Changed (breaking)
61
+
62
+ - **`session.dataRefresh` requires `session.dataSchema`**, the zod schema of
63
+ `session.data`, at compile time and at creation. Every read checks the
64
+ record's data against it and hands the request the schema's output; data
65
+ that does not match (a record written before a deploy changed its shape)
66
+ is refreshed on that read, whatever its deadline, and a refresh whose
67
+ output the schema refuses fails the read as a
68
+ `LambderSessionDataRefreshError`, writing nothing. Without `dataRefresh`
69
+ the schema is optional, and data that does not match ends the session.
70
+ With a schema, `createSession` and `updateSessionData` throw on data it
71
+ refuses and write nothing, rather than store a session its next read
72
+ would end. A record keeps its data as written or refreshed, and requests
73
+ are served the schema's output, so every read parses exactly what its
74
+ write was checked with. The same holds for the mock runtime's sessions.
75
+ - To move: give `dataSchema` beside `dataRefresh`, the schema your
76
+ `SessionData` type is, and derive the type from it
77
+ (`z.infer<typeof sessionDataSchema>`) so the two cannot drift.
78
+ - **The DynamoDB rate limiter sends every window's write at once**, so a
79
+ policy capping several windows costs one round trip rather than one per
80
+ window. A window under its limit now counts an attempt another window
81
+ refuses (the smaller windows were counted first, and the larger ones
82
+ spared, before), and a window the table refused an attempt in is
83
+ remembered by the process until it resets, so a flood's later attempts
84
+ are refused without touching the table and stop raising the other
85
+ windows' counters. The memory limiter checks every window before counting
86
+ any, so an attempt it refuses counts nowhere.
87
+ - **`LambderApiAnswer` and `LambderResponse` carry the answer's outcome**
88
+ (`outcome`, `callOutcome`), which the envelope builders and
89
+ `res.apiRefusal()` set, and `LambderInstanceBackends` takes a
90
+ `callSummary` writer.
91
+
92
+ ### Added
93
+
94
+ - **One summary line per API call** (`callSummary`), written when the call
95
+ is answered: the endpoint, how it ended (`success`, `refusal` and its
96
+ code, a flag, `validation`, `crash`, a failing `created` hook's included),
97
+ the status, the duration and the handler's own time, whether an
98
+ idempotent answer was replayed, whether the invocation was the process's
99
+ first, and its request id with the id of the invocation that invoked it.
100
+ Nothing from the input, the session, the cookies or the caller's address.
101
+ By default each is one JSON line on stdout, which CloudWatch Logs Insights
102
+ reads field by field; a function receives each instead, and `false`
103
+ writes none. A test app collects them in `app.callSummaries`.
104
+ - **The calling invocation's request id travels over invoke**
105
+ (`LAMBDER_PARENT_REQUEST_HEADER`), read by the callee from an invoke
106
+ alone, so the two functions' summary lines join.
107
+ - **`originProof: { header, secrets }`**: the trusted client address and
108
+ host headers are read only from a request carrying the secret the proxy in
109
+ front of the app sets, so a request sent to the origin directly (an
110
+ `execute-api` or `lambda-url` address) is counted by the address it came
111
+ from. The header is taken off `ctx.headers`; `secrets` takes the previous
112
+ secret during a rotation; a secret under 32 characters, a proof guarding
113
+ no trusted header, and a proof header that is also a trusted one are
114
+ refused at creation.
115
+ - **Refusal codes as a list of maps**: `declareRefusals([orderRefusals,
116
+ walletRefusals])`, on the server's init and the mock's alike, so each part
117
+ of an app declares its own codes, as guards and rate-limit policies are
118
+ declared. A code two maps declare is a compile error and a throw.
119
+ - **Every translation keeps the default language's placeholders.** A block
120
+ whose text drops or adds a `{token}` against the default language's text
121
+ of the same key is refused where it arrives: an inline block when the
122
+ instance or an extension is created, a loaded one when its loader answers
123
+ (`loadLanguage` rejects, and the language stays on the default one), a
124
+ registered one at `registerDictionary`. Each error lists every mismatch of
125
+ the block.
126
+ - **`generateApiFiles(config, { check })`** in `lambder/build`: every
127
+ contract, signature file, options file and guard-parameter file of every
128
+ app a generator script names, each app's module given once, in one call
129
+ that names every stale or broken file and does not stop at the first.
130
+
12
131
  ## [12.0.1] - 2026-09-30
13
132
 
14
133
  A major: endpoints are values. An app declares each endpoint with its
package/README.md CHANGED
@@ -65,7 +65,8 @@ const company = await caller.companies.get({ slug: "acme" });
65
65
  - **Simple route and API declaration.** Paths, regexes, predicates and
66
66
  structured matchers, chained fluently.
67
67
  - **Sessions.** Over a store of your choosing (DynamoDB, memory, your own),
68
- with secrets hashed at rest, sliding expiration, data refresh and
68
+ with secrets hashed at rest, sliding expiration, session data checked
69
+ against its schema on every read and refreshed from its source, and
69
70
  cross-subdomain cookies.
70
71
  - **One API core, two runtimes.** The request pipeline (envelope, refusals,
71
72
  sessions, guards, rate limits, idempotency) is one isomorphic class; the
@@ -79,6 +80,10 @@ const company = await caller.companies.get({ slug: "acme" });
79
80
  cookies, and a guard against Lambda's response size cap.
80
81
  - **Hooks and actions.** Lifecycle hooks, plus `addAction()` for the non-HTTP
81
82
  invocations (EventBridge, SQS, custom events) the same function receives.
83
+ - **A summary line per call.** Every API call is logged once, as JSON, with
84
+ its endpoint, outcome, refusal code, status and timings, and the request
85
+ ids that join it to the calls it made; nothing from its input or its
86
+ caller.
82
87
  - **Lambda to lambda calls.** `LambderInvokeCaller` invokes a Lambder app in
83
88
  another function directly, with no API Gateway in between, typed from the
84
89
  callee's own contract and carrying its refusals, crash detail and logs back.
@@ -135,7 +140,7 @@ The package ships five entry points; pick by where the code runs:
135
140
  | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `LambderUploadRunner`, `LambderBackoffTimer`, `LambderSignedClaims`, `html`/`xml` tagged templates, `createLambderI18n` |
136
141
  | `lambder/mock` | Browser and Node, in development and tests | `LambderMockApp`, the mock runtime: your typed contract served from mock handlers over the real API pipeline and memory stores |
137
142
  | `lambder/testing` | Node, in tests | `lambderTestApp`: your real instance under test in this process, memory stores put under it in place, simulated browsers with typed callers in front of it, and the outcome assertions; the store conformance suites, to hold a store you write to the rules Lambder's own meet |
138
- | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters and nothing else, what a browser gating on that guard carries; `writeApiContract`: the contract as plain types a client compiles instead of the server |
143
+ | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters, one export per API and nothing else, so a browser gating on that guard carries only the ones it imports; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call |
139
144
 
140
145
  Frontends and shared isomorphic packages should import from `lambder/client`
141
146
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
@@ -1,3 +1,4 @@
1
+ import type { LambderCallOutcomeHint } from "../shared/wire/LambderCallOutcome.js";
1
2
  import type { LambderApiHttpAnswer } from "../shared/wire/LambderApiOutcome.js";
2
3
  /**
3
4
  * The core's output for one API call, before any transport-level
@@ -23,6 +24,12 @@ export type LambderApiAnswer = {
23
24
  isBodyBase64?: boolean;
24
25
  compress?: boolean | "auto";
25
26
  etag?: boolean | "auto";
27
+ /**
28
+ * What the answer is, as the code that wrote it knows it, for the call's
29
+ * summary line: set by the envelope and validation builders, dropped by
30
+ * the stores like the other hints (a replay reads it back from the text).
31
+ */
32
+ outcome?: LambderCallOutcomeHint;
26
33
  };
27
34
  /**
28
35
  * An answer in the accessor form resolveApiOutcome() reads (the same view a
@@ -1,3 +1,4 @@
1
+ import { outcomeOfEnvelope } from "../shared/wire/LambderCallOutcome.js";
1
2
  import { LAMBDER_REFUSAL_CODES, isLambderRefusalCode, refusalMessageOf } from "../shared/wire/LambderApiRefusal.js";
2
3
  import { setAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
3
4
  /*
@@ -73,7 +74,8 @@ export const envelopeAnswer = (envelope, options = {}) => {
73
74
  // casing replaces the envelope's own rather than shipping beside it.
74
75
  setAnswerHeader(headers, key, value);
75
76
  }
76
- return { statusCode: options.statusCode ?? 200, headers, body: JSON.stringify(envelope) };
77
+ const statusCode = options.statusCode ?? 200;
78
+ return { statusCode, headers, body: JSON.stringify(envelope), outcome: outcomeOfEnvelope(envelope, statusCode) };
77
79
  };
78
80
  /**
79
81
  * A thrown refusal as an answer: its refusal and flags on the envelope,
@@ -187,6 +189,7 @@ export const validationAnswer = (zodError, logList) => {
187
189
  ...(trimmed ? { issueCount: zodError.issues.length } : {}),
188
190
  ...(logList?.length ? { logList } : {}),
189
191
  }),
192
+ outcome: { outcome: "validation", code: null },
190
193
  };
191
194
  };
192
195
  /** No API is registered under the requested name: a refusal, not a 404, so a typed caller reads it. */
@@ -3,6 +3,7 @@ import type { LambderJsonOf } from "../shared/wire/LambderApiContract.js";
3
3
  import type { LambderRefusalStatusCode } from "../shared/wire/LambderHttpStatus.js";
4
4
  import type { LambderNoExtraKeys } from "../shared/util/LambderTypeUtilities.js";
5
5
  import { LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
6
+ import { type LambderMergedNamedMaps, type LambderNamedMapsOption, type LambderNoRepeatedNames } from "../shared/util/LambderNamedMaps.js";
6
7
  /**
7
8
  * One code of an app's refusal vocabulary: the schema of the data it carries,
8
9
  * when it carries data, and how every refusal with the code leaves the
@@ -69,6 +70,27 @@ export type LambderRefusalVocabularyChecks<TVocabulary> = {
69
70
  readonly "lambder: a refusal code may not start with lambder/, the prefix of the framework's own codes": never;
70
71
  } : LambderNoExtraKeys<TVocabulary[TCode], LambderRefusalDeclaration> & LambderRefusalDataCheck<TVocabulary[TCode]>;
71
72
  };
73
+ /**
74
+ * The vocabulary as declareRefusals() takes it: one map of codes, or a list of
75
+ * them, so each part of an app declares its own codes beside its APIs. A
76
+ * code two maps declare is refused.
77
+ */
78
+ export type LambderRefusalVocabularyOption = LambderNamedMapsOption<LambderRefusalVocabulary>;
79
+ /** The one vocabulary a declareRefusals() option declares: a list's maps merged, a lone map as it is. */
80
+ export type LambderMergedRefusalVocabulary<TOption> = LambderMergedNamedMaps<TOption> extends infer TMerged extends LambderRefusalVocabulary ? TMerged : never;
81
+ /** A declareRefusals() option checked code by code, in each map of a list, and a list declaring no code twice. */
82
+ export type LambderRefusalVocabularyOptionChecks<TOption> = TOption extends readonly unknown[] ? {
83
+ [TIndex in keyof TOption]: LambderRefusalVocabularyChecks<TOption[TIndex]>;
84
+ } & LambderNoRepeatedNames<TOption> : LambderRefusalVocabularyChecks<TOption>;
85
+ /**
86
+ * A declareRefusals() option as the one vocabulary it declares, checked
87
+ * (see readRefusalVocabulary) and keyed. `declarer` names the init in the
88
+ * errors.
89
+ */
90
+ export declare const declaredRefusalVocabulary: (option: LambderRefusalVocabularyOption, declarer: string) => {
91
+ refusals: LambderRefusalVocabulary;
92
+ vocabulary: Map<string, LambderRefusalDeclaration>;
93
+ };
72
94
  /** One code's declaration as checkedRefusal reads it: how a refusal with the code leaves, and its schema. */
73
95
  export declare const allowedRefusalOf: (declaration: LambderRefusalDeclaration) => LambderApiAllowedRefusal;
74
96
  /**
@@ -1,5 +1,18 @@
1
1
  import { LambderApiRefusal, isLambderRefusalCode } from "../shared/wire/LambderApiRefusal.js";
2
2
  import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
3
+ import { mergeNamedMaps } from "../shared/util/LambderNamedMaps.js";
4
+ /**
5
+ * A declareRefusals() option as the one vocabulary it declares, checked
6
+ * (see readRefusalVocabulary) and keyed. `declarer` names the init in the
7
+ * errors.
8
+ */
9
+ export const declaredRefusalVocabulary = (option, declarer) => {
10
+ if (option === null || typeof option !== "object") {
11
+ throw new Error(`${declarer}: declareRefusals() takes the vocabulary, an object of codes or a list of them.`);
12
+ }
13
+ const refusals = mergeNamedMaps(option, "refusal code", "declareRefusals()");
14
+ return { refusals, vocabulary: readRefusalVocabulary(refusals) };
15
+ };
3
16
  /** One code's declaration as checkedRefusal reads it: how a refusal with the code leaves, and its schema. */
4
17
  export const allowedRefusalOf = (declaration) => {
5
18
  const leaves = {
@@ -0,0 +1,60 @@
1
+ import type { LambderModuleLocation } from "./moduleLocation.js";
2
+ import { type LambderApiContractFileOptions } from "./writeApiContract.js";
3
+ import { type LambderApiSignatureFileOptions } from "./writeApiSignatures.js";
4
+ import { type LambderApiOptionsFileOptions } from "./writeApiOptions.js";
5
+ import { type LambderApiGuardParamsFileOptions } from "./writeApiGuardParams.js";
6
+ /** What a file's writer takes beyond where the instance is, which the app gives once for all its files. */
7
+ type LambderAppFile<TOptions> = Omit<TOptions, "module" | "exportName" | "tsconfig" | "check">;
8
+ /** One app: where its instance is, and which files it is written to. Each file is optional. */
9
+ export type LambderApiFilesApp = {
10
+ /** The module that exports the instance, usually the server's entry: a path relative to the working directory, or a file URL. */
11
+ module: LambderModuleLocation;
12
+ /** The export that holds the instance. Default: "default". */
13
+ exportName?: string;
14
+ /** The tsconfig.json the module compiles under, which the contract is read through. Default: the nearest one at or above the module. */
15
+ tsconfig?: string;
16
+ /** The contract a client compiles against, as plain types (writeApiContract). */
17
+ contract?: LambderAppFile<LambderApiContractFileOptions>;
18
+ /** The signature file both sides ship (writeApiSignatures). */
19
+ signatures?: LambderAppFile<LambderApiSignatureFileOptions>;
20
+ /** The declared options as plain data, for a mock and tests (writeApiOptions). */
21
+ options?: LambderAppFile<LambderApiOptionsFileOptions>;
22
+ /** One file per guard whose parameters a browser needs (writeApiGuardParams). */
23
+ guardParams?: readonly LambderAppFile<LambderApiGuardParamsFileOptions>[];
24
+ };
25
+ /** The files generateApiFiles writes: every app's, by a name the output lines use. */
26
+ export type LambderApiFilesConfig = {
27
+ apps: Record<string, LambderApiFilesApp>;
28
+ };
29
+ export type LambderApiFilesResult = {
30
+ /** False when a check found a file stale, or a writer failed. */
31
+ ok: boolean;
32
+ /** What happened, per app and file, as lines to print. */
33
+ lines: string[];
34
+ };
35
+ /**
36
+ * Writes every file the config names, or with `check` writes nothing and
37
+ * answers whether each one on disk is current. Paths are relative to the
38
+ * working directory, as each writer takes them.
39
+ *
40
+ * ```ts
41
+ * import { generateApiFiles } from "lambder/build";
42
+ *
43
+ * const result = await generateApiFiles({
44
+ * apps: {
45
+ * server: {
46
+ * module: "backend/index.ts",
47
+ * exportName: "lambder",
48
+ * contract: { file: "shared/generated/apiContract.generated.ts" },
49
+ * signatures: { file: "shared/generated/apiSignatures.generated.ts" },
50
+ * },
51
+ * },
52
+ * }, { check: process.argv.includes("--check") });
53
+ * console.log(result.lines.join("\n"));
54
+ * process.exit(result.ok ? 0 : 1);
55
+ * ```
56
+ */
57
+ export declare const generateApiFiles: (config: LambderApiFilesConfig, options?: {
58
+ check?: boolean;
59
+ }) => Promise<LambderApiFilesResult>;
60
+ export {};
@@ -0,0 +1,85 @@
1
+ import { writeApiContract } from "./writeApiContract.js";
2
+ import { writeApiSignatures } from "./writeApiSignatures.js";
3
+ import { writeApiOptions } from "./writeApiOptions.js";
4
+ import { writeApiGuardParams } from "./writeApiGuardParams.js";
5
+ const APP_KEYS = ["module", "exportName", "tsconfig", "contract", "signatures", "options", "guardParams"];
6
+ const FILE_KINDS = ["contract", "signatures", "options", "guardParams"];
7
+ /** A config that names apps, each with its module and at least one file, and no key the generator does not read. */
8
+ function assertConfig(config) {
9
+ const apps = config?.apps;
10
+ if (apps === null || typeof apps !== "object" || Array.isArray(apps) || Object.keys(apps).length === 0) {
11
+ throw new Error("generateApiFiles: the config names no apps. Give `apps`, one entry per Lambder instance: { module, exportName, and the files it is written to }.");
12
+ }
13
+ for (const [name, app] of Object.entries(apps)) {
14
+ if (app === null || typeof app !== "object")
15
+ throw new Error(`generateApiFiles: the app "${name}" is not an object.`);
16
+ const unknownKeys = Object.keys(app).filter((key) => !APP_KEYS.includes(key));
17
+ if (unknownKeys.length)
18
+ throw new Error(`generateApiFiles: the app "${name}" has ${unknownKeys.map((key) => `"${key}"`).join(", ")}, which the generator does not read (it reads ${APP_KEYS.join(", ")}).`);
19
+ if (!app.module)
20
+ throw new Error(`generateApiFiles: the app "${name}" names no module to read its instance from.`);
21
+ if (!FILE_KINDS.some((kind) => app[kind] !== undefined)) {
22
+ throw new Error(`generateApiFiles: the app "${name}" is written to no file; give it a contract, signatures, options or guardParams.`);
23
+ }
24
+ }
25
+ }
26
+ /** A writer's run as lines: its own on success or a stale check, the reason when it threw. */
27
+ const settle = async (label, write) => {
28
+ try {
29
+ return await write();
30
+ }
31
+ catch (err) {
32
+ const reasons = [];
33
+ for (let cause = err; cause instanceof Error && reasons.length < 4; cause = cause.cause)
34
+ reasons.push(cause.message);
35
+ return { ok: false, lines: [`✗ ${label} failed: ${reasons.join(": ") || String(err)}`] };
36
+ }
37
+ };
38
+ /**
39
+ * Writes every file the config names, or with `check` writes nothing and
40
+ * answers whether each one on disk is current. Paths are relative to the
41
+ * working directory, as each writer takes them.
42
+ *
43
+ * ```ts
44
+ * import { generateApiFiles } from "lambder/build";
45
+ *
46
+ * const result = await generateApiFiles({
47
+ * apps: {
48
+ * server: {
49
+ * module: "backend/index.ts",
50
+ * exportName: "lambder",
51
+ * contract: { file: "shared/generated/apiContract.generated.ts" },
52
+ * signatures: { file: "shared/generated/apiSignatures.generated.ts" },
53
+ * },
54
+ * },
55
+ * }, { check: process.argv.includes("--check") });
56
+ * console.log(result.lines.join("\n"));
57
+ * process.exit(result.ok ? 0 : 1);
58
+ * ```
59
+ */
60
+ export const generateApiFiles = async (config, options = {}) => {
61
+ assertConfig(config);
62
+ const check = options.check ?? false;
63
+ const apps = Object.entries(config.apps);
64
+ const runs = [];
65
+ // The contracts first: they compile the app's sources and run none of
66
+ // them, so a module that fails to load still gets its contract checked.
67
+ for (const [name, app] of apps) {
68
+ if (!app.contract)
69
+ continue;
70
+ runs.push(await settle(`${name}: the contract`, () => writeApiContract({ ...app.contract, module: app.module, exportName: app.exportName, tsconfig: app.tsconfig, check })));
71
+ }
72
+ // Then what is read off the instance itself, which importing the module
73
+ // builds once for all three.
74
+ for (const [name, app] of apps) {
75
+ const instance = { module: app.module, exportName: app.exportName, check };
76
+ if (app.signatures)
77
+ runs.push(await settle(`${name}: the signatures`, () => writeApiSignatures({ ...app.signatures, ...instance })));
78
+ if (app.options)
79
+ runs.push(await settle(`${name}: the options`, () => writeApiOptions({ ...app.options, ...instance })));
80
+ for (const guardFile of app.guardParams ?? []) {
81
+ runs.push(await settle(`${name}: the "${guardFile.guard}" guard parameters`, () => writeApiGuardParams({ ...guardFile, ...instance })));
82
+ }
83
+ }
84
+ return { ok: runs.every((run) => run.ok), lines: runs.flatMap((run) => run.lines) };
85
+ };
@@ -52,7 +52,7 @@ export declare const settleGeneratedFile: (run: {
52
52
  previousText: string | null;
53
53
  /** Whether that text held the tables as written; false for a file rewritten by hand. */
54
54
  readBack: boolean;
55
- /** Each table the file holds, under its exported name: how many entries it has now, and which of them moved. */
55
+ /** Each table the file holds, under the name its lines print (a table's exported name): how many entries it has now, and which of them moved. */
56
56
  tables: readonly {
57
57
  name: string;
58
58
  count: number;
@@ -12,13 +12,13 @@ export type LambderApiGuardParamsFileOptions = {
12
12
  exportName?: string;
13
13
  /** The guard whose parameters the file holds, as the server declares it. A name the server declares no guard under throws. */
14
14
  guard: string;
15
- /** The TypeScript module to write, exporting `guardParams`. Relative to the working directory. */
15
+ /** The TypeScript module to write, one export per API that declares the guard. Relative to the working directory. */
16
16
  file: string;
17
17
  /** Write nothing, and answer whether the file on disk holds what the instance reports now. Default: false. */
18
18
  check?: boolean;
19
19
  /** The comment the file opens with, one `//` line per line. It should name what generates the file. Default: a note naming writeApiGuardParams() and the guard. */
20
20
  header?: string;
21
- /** End the generated statement with a semicolon. Default: true. The table itself is JSON, double quotes included, whatever the project's style: that is what lets a check read it back. */
21
+ /** End each generated statement with a semicolon. Default: true. The values are JSON, double quotes included, whatever the project's style: that is what lets a check read them back. */
22
22
  semicolons?: boolean;
23
23
  };
24
24
  export type LambderApiGuardParamsFileResult = {
@@ -26,7 +26,7 @@ export type LambderApiGuardParamsFileResult = {
26
26
  ok: boolean;
27
27
  /** The absolute path. */
28
28
  file: string;
29
- /** True when the file was (re)written; a file that already holds this table is left untouched, however it is formatted. */
29
+ /** True when the file was (re)written; a file that already holds these parameters is left untouched, however it is formatted. */
30
30
  written: boolean;
31
31
  /** How many APIs declare the guard. */
32
32
  count: number;
@@ -36,8 +36,8 @@ export type LambderApiGuardParamsFileResult = {
36
36
  lines: string[];
37
37
  };
38
38
  /**
39
- * Writes one guard's parameters as a module of plain data, or checks the one
40
- * on disk, and says which APIs moved.
39
+ * Writes one guard's parameters as a module of one export per API, or checks
40
+ * the one on disk, and says which APIs moved.
41
41
  *
42
42
  * ```ts
43
43
  * import { writeApiGuardParams } from "lambder/build";
@@ -51,10 +51,11 @@ export type LambderApiGuardParamsFileResult = {
51
51
  * });
52
52
  * ```
53
53
  *
54
- * The module exports `guardParams`, `as const`, so a client reads its types
55
- * straight off it: `keyof typeof guardParams` is the APIs behind the guard,
56
- * and `(typeof guardParams)[K]` the literal API K declared, `true` for a
57
- * guard named without a parameter (the string and list forms). A parameter
58
- * that is not plain data fails the write, as it does for writeApiOptions.
54
+ * API `orders.list` exports `ordersListGuardParam`: the literal it declared,
55
+ * `true` for a guard named without a parameter (the string and list forms),
56
+ * typed LambderApiGuardParam<"orders.list", "store", literal>. Two APIs whose
57
+ * names export under one identifier fail the write, and so does a parameter
58
+ * that is not plain data, as it does for writeApiOptions, or that is null,
59
+ * which no tag can carry.
59
60
  */
60
61
  export declare const writeApiGuardParams: (options: LambderApiGuardParamsFileOptions) => Promise<LambderApiGuardParamsFileResult>;