lambder 9.0.3 → 10.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 +195 -0
- package/README.md +25 -11
- package/dist/api/LambderApiDefinition.d.ts +9 -0
- package/dist/api/LambderApiEnvelope.d.ts +22 -10
- package/dist/api/LambderApiEnvelope.js +57 -22
- package/dist/api/LambderApiGuards.d.ts +77 -22
- package/dist/api/LambderApiGuards.js +13 -1
- package/dist/api/LambderApiIdempotency.js +4 -4
- package/dist/api/LambderApiOutputValidationError.d.ts +10 -4
- package/dist/api/LambderApiOutputValidationError.js +14 -6
- package/dist/api/LambderApiPipeline.d.ts +2 -1
- package/dist/api/LambderApiPipeline.js +10 -2
- package/dist/api/LambderApiRateLimits.d.ts +15 -11
- package/dist/api/LambderApiRateLimits.js +31 -7
- package/dist/api/LambderApiRefusals.d.ts +204 -0
- package/dist/api/LambderApiRefusals.js +223 -0
- package/dist/api/LambderApiSignature.d.ts +13 -4
- package/dist/api/LambderApiSignature.js +21 -4
- package/dist/build/writeApiOptions.js +7 -5
- package/dist/client/LambderCaller.d.ts +28 -17
- package/dist/client/LambderCaller.js +19 -15
- package/dist/client.d.ts +5 -4
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +121 -23
- package/dist/core/Lambder.js +105 -51
- package/dist/core/LambderContext.d.ts +1 -1
- package/dist/core/LambderContext.js +5 -0
- package/dist/core/LambderCreateOptions.d.ts +58 -15
- package/dist/core/LambderCreateOptions.js +10 -0
- package/dist/core/LambderPolicyBuilders.d.ts +11 -8
- package/dist/core/LambderPolicyBuilders.js +10 -8
- package/dist/core/LambderResolver.d.ts +1 -1
- package/dist/core/LambderResolver.js +1 -1
- package/dist/core/LambderResponseBuilder.d.ts +11 -9
- package/dist/core/LambderResponseBuilder.js +12 -11
- package/dist/index.d.ts +12 -8
- package/dist/index.js +6 -2
- package/dist/invoke/LambderInvokeCaller.d.ts +5 -6
- package/dist/invoke/LambderInvokeCaller.js +13 -12
- package/dist/invoke/LambderInvokeOutcome.d.ts +32 -23
- package/dist/invoke/LambderInvokeOutcome.js +4 -4
- package/dist/mock/LambderMockApp.d.ts +86 -24
- package/dist/mock/LambderMockApp.js +108 -16
- package/dist/mock/LambderMockCallRecorder.js +6 -3
- package/dist/mock/LambderMockCreateOptions.d.ts +4 -5
- package/dist/mock/LambderMockFailureInjector.d.ts +9 -2
- package/dist/mock/LambderMockFailureInjector.js +18 -4
- package/dist/mock/LambderMockTypes.d.ts +34 -11
- package/dist/mock.d.ts +4 -3
- package/dist/mock.js +4 -3
- package/dist/shared/util/LambderTypeUtilities.d.ts +11 -0
- package/dist/shared/wire/LambderApiContract.d.ts +96 -31
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +19 -12
- package/dist/shared/wire/LambderApiOptionValues.d.ts +15 -6
- package/dist/shared/wire/LambderApiOptionValues.js +3 -3
- package/dist/shared/wire/LambderApiOutcome.d.ts +49 -27
- package/dist/shared/wire/LambderApiOutcome.js +28 -11
- package/dist/shared/wire/LambderApiRefusal.d.ts +127 -42
- package/dist/shared/wire/LambderApiRefusal.js +15 -7
- package/dist/shared/wire/LambderCrashDetail.d.ts +1 -1
- package/dist/shared/wire/LambderCrashDetail.js +1 -1
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +3 -2
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -5
- package/dist/shared/wire/LambderObjectPayload.d.ts +15 -0
- package/dist/shared/wire/LambderObjectPayload.js +24 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +52 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +34 -8
- package/dist/testing/LambderTestApp.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { LambderJsonOf } from "../shared/wire/LambderApiContract.js";
|
|
3
|
+
import type { LambderRefusalStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
4
|
+
import type { LambderNoExtraKeys } from "../shared/util/LambderTypeUtilities.js";
|
|
5
|
+
import { LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
6
|
+
/**
|
|
7
|
+
* One code of an app's refusal vocabulary: the schema of the data it carries,
|
|
8
|
+
* when it carries data, and how every refusal with the code leaves the
|
|
9
|
+
* server. A code's data is an object or an array, as an API's output is. The
|
|
10
|
+
* status and the flag are declared once here rather than at each raise site,
|
|
11
|
+
* so a code always reaches a caller the same way.
|
|
12
|
+
*/
|
|
13
|
+
export type LambderRefusalDeclaration = {
|
|
14
|
+
/** The schema of the code's data, when it carries data. */
|
|
15
|
+
data?: z.ZodType;
|
|
16
|
+
/** The HTTP status every refusal with this code leaves with. Default 200: the envelope is the channel. */
|
|
17
|
+
status?: LambderRefusalStatusCode;
|
|
18
|
+
/** Whether every refusal with this code sets the envelope's notAuthorized flag, which a caller routes to its notAuthorizedHandler. */
|
|
19
|
+
notAuthorized?: true;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* One code as its endpoint may send it: the status and flag its declaration
|
|
23
|
+
* gives it, and whether it carries data, with the data's schema when it does.
|
|
24
|
+
* The server and the mock both resolve it from the vocabulary
|
|
25
|
+
* (allowedRefusalOf), so a refusal's data is parsed the same way on both.
|
|
26
|
+
*/
|
|
27
|
+
export type LambderApiAllowedRefusal = {
|
|
28
|
+
/** The status every refusal with the code leaves with; 200 when absent. */
|
|
29
|
+
status?: LambderRefusalStatusCode;
|
|
30
|
+
/** Present when every refusal with the code sets the envelope's notAuthorized flag. */
|
|
31
|
+
notAuthorized?: true;
|
|
32
|
+
} & ({
|
|
33
|
+
data: false;
|
|
34
|
+
} | {
|
|
35
|
+
data: true;
|
|
36
|
+
schema: z.ZodType;
|
|
37
|
+
});
|
|
38
|
+
/** The codes one endpoint may refuse with: its own `refusals` option and those of the guards it declares. */
|
|
39
|
+
export type LambderApiAllowedRefusals = ReadonlyMap<string, LambderApiAllowedRefusal>;
|
|
40
|
+
/** The app's refusal vocabulary, as `initLambder().declareRefusals()` and the mock's take it: every code once, with its declaration. */
|
|
41
|
+
export type LambderRefusalVocabulary = Record<string, LambderRefusalDeclaration>;
|
|
42
|
+
/**
|
|
43
|
+
* What declareRefusals() hands the rest of an init, the server's and the
|
|
44
|
+
* mock's alike: the vocabulary as declared, checked and keyed, and whether
|
|
45
|
+
* every refusal an API answers with has to name a code.
|
|
46
|
+
*/
|
|
47
|
+
export type LambderDeclaredVocabulary<TRefusals extends LambderRefusalVocabulary, TCodesRequired extends boolean> = {
|
|
48
|
+
refusals: TRefusals;
|
|
49
|
+
vocabulary: ReadonlyMap<string, LambderRefusalDeclaration>;
|
|
50
|
+
requireCodes: TCodesRequired;
|
|
51
|
+
};
|
|
52
|
+
/** A refusal's data is an object or an array, as an API's output is: the same rule, where the code is declared. */
|
|
53
|
+
type LambderRefusalDataCheck<TDeclaration> = TDeclaration extends {
|
|
54
|
+
data: infer TSchema extends z.ZodType;
|
|
55
|
+
} ? 0 extends 1 & z.output<TSchema> ? unknown : undefined extends z.output<TSchema> ? LambderRefusalDataRefusal : [LambderJsonOf<z.output<TSchema>>] extends [never] ? LambderRefusalDataRefusal : [LambderJsonOf<z.output<TSchema>>] extends [object] ? unknown : LambderRefusalDataRefusal : unknown;
|
|
56
|
+
type LambderRefusalDataRefusal = {
|
|
57
|
+
data: {
|
|
58
|
+
readonly "lambder: a refusal's data is an object or an array, as an API's output is.": never;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The vocabulary as declareRefusals() checks it, code by code: no `lambder/`
|
|
63
|
+
* prefix, no key beside data, status and notAuthorized (a `dat:` would leave
|
|
64
|
+
* a code that carries data declared as one that does not), and data that is
|
|
65
|
+
* an object or an array.
|
|
66
|
+
*/
|
|
67
|
+
export type LambderRefusalVocabularyChecks<TVocabulary> = {
|
|
68
|
+
[TCode in keyof TVocabulary]: TCode extends `lambder/${string}` ? {
|
|
69
|
+
readonly "lambder: a refusal code may not start with lambder/, the prefix of the framework's own codes": never;
|
|
70
|
+
} : LambderNoExtraKeys<TVocabulary[TCode], LambderRefusalDeclaration> & LambderRefusalDataCheck<TVocabulary[TCode]>;
|
|
71
|
+
};
|
|
72
|
+
/** One code's declaration as checkedRefusal reads it: how a refusal with the code leaves, and its schema. */
|
|
73
|
+
export declare const allowedRefusalOf: (declaration: LambderRefusalDeclaration) => LambderApiAllowedRefusal;
|
|
74
|
+
/**
|
|
75
|
+
* What one endpoint's refusals are checked against: the codes it may refuse
|
|
76
|
+
* with, and whether every refusal it answers with has to name one
|
|
77
|
+
* (declareRefusals's `requireCodes`). Undefined on a definition whose codes
|
|
78
|
+
* are not known (a mock without the generated tables), which checks nothing.
|
|
79
|
+
*/
|
|
80
|
+
export type LambderEndpointRefusals = {
|
|
81
|
+
codes: LambderApiAllowedRefusals;
|
|
82
|
+
codeRequired: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** How a refusal broke its endpoint's declaration. */
|
|
85
|
+
type LambderRefusalViolation = {
|
|
86
|
+
uncoded: true;
|
|
87
|
+
} | {
|
|
88
|
+
undeclared: string;
|
|
89
|
+
} | {
|
|
90
|
+
dataWithoutDeclaredCode: string | undefined;
|
|
91
|
+
} | {
|
|
92
|
+
missingData: string;
|
|
93
|
+
} | {
|
|
94
|
+
ownStatusOrFlag: true;
|
|
95
|
+
} | {
|
|
96
|
+
zodError: z.ZodError;
|
|
97
|
+
} | {
|
|
98
|
+
thrown: unknown;
|
|
99
|
+
} | {
|
|
100
|
+
notObject: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* A refusal an endpoint is not allowed to send, so it was not sent and the
|
|
104
|
+
* call is answered as a crash: no code where the app requires one, a code
|
|
105
|
+
* the endpoint does not declare (neither
|
|
106
|
+
* in its own `refusals` option nor through a guard), data on a refusal whose
|
|
107
|
+
* code declares none, no data on one whose code carries data, data its code's
|
|
108
|
+
* schema rejects or that is not an object or an array, or a declared code
|
|
109
|
+
* raised with a status or flag of its own, which its declaration owns.
|
|
110
|
+
*
|
|
111
|
+
* What makes a client's refusal type exact: a reader narrows `code` to the
|
|
112
|
+
* endpoint's declared codes and the framework's own, and relies on this to
|
|
113
|
+
* never see another. The refusal side's counterpart of
|
|
114
|
+
* LambderApiOutputValidationError. The refusal as thrown is the cause, so
|
|
115
|
+
* the stack points at the line that raised it.
|
|
116
|
+
*/
|
|
117
|
+
export declare class LambderApiRefusalValidationError extends Error {
|
|
118
|
+
readonly apiName: string;
|
|
119
|
+
/** The code the refusal carried, if any. */
|
|
120
|
+
readonly code: string | undefined;
|
|
121
|
+
/** The schema's issues when the code's schema rejected the data; null otherwise. */
|
|
122
|
+
readonly zodError: z.ZodError | null;
|
|
123
|
+
constructor(apiName: string, thrown: LambderApiRefusal, violation: LambderRefusalViolation);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The refusal as endpoint `apiName` may send it, or a thrown
|
|
127
|
+
* LambderApiRefusalValidationError when it may not. Where a thrown refusal
|
|
128
|
+
* becomes an answer for a known endpoint (the pipeline, a hook refusing an
|
|
129
|
+
* API call on the server, an injected failure in the mock), it passes through
|
|
130
|
+
* here first.
|
|
131
|
+
*
|
|
132
|
+
* - An uncoded refusal, or one carrying a framework code, goes out as it is,
|
|
133
|
+
* provided it carries no data. Where the app requires codes, an uncoded
|
|
134
|
+
* refusal is refused too; a framework code still passes.
|
|
135
|
+
* - A declared code leaves with its declaration's status and notAuthorized
|
|
136
|
+
* flag, and with data exactly when the code carries data, parsed through
|
|
137
|
+
* the code's schema as an output is: undeclared fields stripped, defaults
|
|
138
|
+
* filled, transforms run. The parse is synchronous, so a data schema cannot
|
|
139
|
+
* be async.
|
|
140
|
+
* - Anything else is refused, a declared code raised with a status or flag of
|
|
141
|
+
* its own included.
|
|
142
|
+
*
|
|
143
|
+
* `endpoint` undefined means the endpoint's codes are not known here (a mock
|
|
144
|
+
* given no generated options), and nothing is checked.
|
|
145
|
+
*/
|
|
146
|
+
export declare const checkedRefusal: (apiName: string, endpoint: LambderEndpointRefusals | undefined, thrown: LambderApiRefusal) => LambderApiRefusal;
|
|
147
|
+
/**
|
|
148
|
+
* Throws when `where` (an API, a guard) names a refusal code the vocabulary
|
|
149
|
+
* does not hold, or names one when there is no vocabulary at all.
|
|
150
|
+
*/
|
|
151
|
+
export declare const assertRefusalCodesDeclared: (where: string, codes: readonly string[], vocabulary: ReadonlyMap<string, LambderRefusalDeclaration> | null) => void;
|
|
152
|
+
/**
|
|
153
|
+
* One endpoint's allowed codes, resolved from the vocabulary: the codes its
|
|
154
|
+
* own `refusals` option names and those of its declared guards, each as
|
|
155
|
+
* checkedRefusal reads it, schema included. Throws on a name the vocabulary
|
|
156
|
+
* does not hold, and on a declaration when there is no vocabulary at all.
|
|
157
|
+
*/
|
|
158
|
+
export declare const resolveAllowedRefusals: (apiName: string, vocabulary: ReadonlyMap<string, LambderRefusalDeclaration> | null, ownCodes: readonly string[], guardCodes: readonly {
|
|
159
|
+
guard: string;
|
|
160
|
+
codes: readonly string[];
|
|
161
|
+
}[]) => LambderApiAllowedRefusals;
|
|
162
|
+
/** The refusals option in its list form: one name becomes a list of one. */
|
|
163
|
+
export declare const toRefusalCodes: (value: string | readonly string[] | undefined) => readonly string[];
|
|
164
|
+
/**
|
|
165
|
+
* The vocabulary, checked: every code a non-empty string outside the
|
|
166
|
+
* framework's `lambder/` prefix, every declaration an object whose `data`,
|
|
167
|
+
* when present, is a zod schema, whose `status`, when present, is one a
|
|
168
|
+
* reader files as a refusal, and whose `notAuthorized`, when present, is
|
|
169
|
+
* true. A Map, so a code named for something Object.prototype carries
|
|
170
|
+
* ("toString") is still an ordinary code.
|
|
171
|
+
*/
|
|
172
|
+
export declare const readRefusalVocabulary: (refusals: Record<string, LambderRefusalDeclaration> | undefined) => Map<string, LambderRefusalDeclaration> | null;
|
|
173
|
+
/** The per-API refusals option: one code of the vocabulary, or a non-empty list of them. Empty is not a declaration, so it is not a form. */
|
|
174
|
+
export type LambderRefusalsOption<TVocabulary> = (keyof TVocabulary & string) | readonly [keyof TVocabulary & string, ...(keyof TVocabulary & string)[]];
|
|
175
|
+
/** The codes a refusals option names, whichever of its two forms is used: toRefusalCodes at the type level. */
|
|
176
|
+
export type LambderRefusalNamesIn<TOpt> = TOpt extends string ? TOpt : TOpt extends readonly (infer TCode extends string)[] ? TCode : never;
|
|
177
|
+
/**
|
|
178
|
+
* The codes as an API handler raises them (ctx.refuse): each mapped to
|
|
179
|
+
* `{ data }` in the schema's input form, the one its transforms take, or to
|
|
180
|
+
* `{}` for a code with no data.
|
|
181
|
+
*/
|
|
182
|
+
export type LambderHandlerRefusalsOf<TVocabulary, TCodes extends string> = {
|
|
183
|
+
[TCode in TCodes]: TCode extends keyof TVocabulary ? TVocabulary[TCode] extends {
|
|
184
|
+
data: infer TSchema extends z.ZodType;
|
|
185
|
+
} ? {
|
|
186
|
+
data: z.input<TSchema>;
|
|
187
|
+
} : {} : never;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* The codes as a contract records them, for the client to narrow on: each
|
|
191
|
+
* mapped to `{ data }` as JSON (the schema's output after its transforms, as
|
|
192
|
+
* it arrives) or to `{}`. never when there are none, so the entry leaves the
|
|
193
|
+
* member out.
|
|
194
|
+
*/
|
|
195
|
+
export type LambderWireRefusalsOf<TVocabulary, TCodes extends string> = [
|
|
196
|
+
TCodes
|
|
197
|
+
] extends [never] ? never : {
|
|
198
|
+
[TCode in TCodes]: TCode extends keyof TVocabulary ? TVocabulary[TCode] extends {
|
|
199
|
+
data: infer TSchema extends z.ZodType;
|
|
200
|
+
} ? {
|
|
201
|
+
data: LambderJsonOf<z.output<TSchema>>;
|
|
202
|
+
} : {} : never;
|
|
203
|
+
};
|
|
204
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { LambderApiRefusal, isLambderRefusalCode } from "../shared/wire/LambderApiRefusal.js";
|
|
2
|
+
import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
|
|
3
|
+
/** One code's declaration as checkedRefusal reads it: how a refusal with the code leaves, and its schema. */
|
|
4
|
+
export const allowedRefusalOf = (declaration) => {
|
|
5
|
+
const leaves = {
|
|
6
|
+
...(declaration.status !== undefined ? { status: declaration.status } : {}),
|
|
7
|
+
...(declaration.notAuthorized ? { notAuthorized: true } : {}),
|
|
8
|
+
};
|
|
9
|
+
return declaration.data === undefined ? { ...leaves, data: false } : { ...leaves, data: true, schema: declaration.data };
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A refusal an endpoint is not allowed to send, so it was not sent and the
|
|
13
|
+
* call is answered as a crash: no code where the app requires one, a code
|
|
14
|
+
* the endpoint does not declare (neither
|
|
15
|
+
* in its own `refusals` option nor through a guard), data on a refusal whose
|
|
16
|
+
* code declares none, no data on one whose code carries data, data its code's
|
|
17
|
+
* schema rejects or that is not an object or an array, or a declared code
|
|
18
|
+
* raised with a status or flag of its own, which its declaration owns.
|
|
19
|
+
*
|
|
20
|
+
* What makes a client's refusal type exact: a reader narrows `code` to the
|
|
21
|
+
* endpoint's declared codes and the framework's own, and relies on this to
|
|
22
|
+
* never see another. The refusal side's counterpart of
|
|
23
|
+
* LambderApiOutputValidationError. The refusal as thrown is the cause, so
|
|
24
|
+
* the stack points at the line that raised it.
|
|
25
|
+
*/
|
|
26
|
+
export class LambderApiRefusalValidationError extends Error {
|
|
27
|
+
apiName;
|
|
28
|
+
/** The code the refusal carried, if any. */
|
|
29
|
+
code;
|
|
30
|
+
/** The schema's issues when the code's schema rejected the data; null otherwise. */
|
|
31
|
+
zodError;
|
|
32
|
+
constructor(apiName, thrown, violation) {
|
|
33
|
+
const code = thrown.refusal.code;
|
|
34
|
+
let message;
|
|
35
|
+
if ("uncoded" in violation) {
|
|
36
|
+
message = `Lambder: API "${apiName}" refused without a code, and this app requires every refusal an API answers with to name a declared code (declareRefusals with requireCodes), so the refusal was not sent. `
|
|
37
|
+
+ "Name one of the API's declared codes, or a framework code.";
|
|
38
|
+
}
|
|
39
|
+
else if ("undeclared" in violation) {
|
|
40
|
+
message = `Lambder: API "${apiName}" refused with the code "${violation.undeclared}", which it does not declare, so the refusal was not sent. `
|
|
41
|
+
+ "Name the code in the API's refusals option (or in a guard it declares), from the refusals vocabulary given at creation.";
|
|
42
|
+
}
|
|
43
|
+
else if ("dataWithoutDeclaredCode" in violation) {
|
|
44
|
+
message = violation.dataWithoutDeclaredCode === undefined
|
|
45
|
+
? `Lambder: API "${apiName}" refused with data but no code, so the refusal was not sent. Only a declared code carries data.`
|
|
46
|
+
: `Lambder: API "${apiName}" refused with the code "${violation.dataWithoutDeclaredCode}" and data it does not declare, so the refusal was not sent.`;
|
|
47
|
+
}
|
|
48
|
+
else if ("missingData" in violation) {
|
|
49
|
+
message = `Lambder: API "${apiName}" refused with the code "${violation.missingData}", which carries data, and no data, so the refusal was not sent.`;
|
|
50
|
+
}
|
|
51
|
+
else if ("ownStatusOrFlag" in violation) {
|
|
52
|
+
message = `Lambder: API "${apiName}" refused with the declared code "${String(code)}" and a status or flag of its own, so the refusal was not sent. `
|
|
53
|
+
+ "A declared code's status and notAuthorized flag are its declaration's: set them in the refusals vocabulary, and leave them off the refuse() call.";
|
|
54
|
+
}
|
|
55
|
+
else if ("zodError" in violation) {
|
|
56
|
+
// Paths and messages only, never the values: they may be the
|
|
57
|
+
// user's data.
|
|
58
|
+
const issues = violation.zodError.issues.slice(0, 3).map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
59
|
+
message = `Lambder: API "${apiName}" refused with the code "${String(code)}" and data its schema does not accept, so the refusal was not sent. ${issues}`;
|
|
60
|
+
}
|
|
61
|
+
else if ("thrown" in violation) {
|
|
62
|
+
message = `Lambder: API "${apiName}" refused with the code "${String(code)}" and data its schema threw on while parsing it (a transform that threw, `
|
|
63
|
+
+ "or an async step: a refusal's data is parsed synchronously), so the refusal was not sent.";
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
message = `Lambder: API "${apiName}" refused with the code "${String(code)}" and data that is ${violation.notObject}, and a refusal's data is an object or an array, so the refusal was not sent.`;
|
|
67
|
+
}
|
|
68
|
+
super(message, { cause: thrown });
|
|
69
|
+
this.name = "LambderApiRefusalValidationError";
|
|
70
|
+
this.apiName = apiName;
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.zodError = "zodError" in violation ? violation.zodError : null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The refusal as endpoint `apiName` may send it, or a thrown
|
|
77
|
+
* LambderApiRefusalValidationError when it may not. Where a thrown refusal
|
|
78
|
+
* becomes an answer for a known endpoint (the pipeline, a hook refusing an
|
|
79
|
+
* API call on the server, an injected failure in the mock), it passes through
|
|
80
|
+
* here first.
|
|
81
|
+
*
|
|
82
|
+
* - An uncoded refusal, or one carrying a framework code, goes out as it is,
|
|
83
|
+
* provided it carries no data. Where the app requires codes, an uncoded
|
|
84
|
+
* refusal is refused too; a framework code still passes.
|
|
85
|
+
* - A declared code leaves with its declaration's status and notAuthorized
|
|
86
|
+
* flag, and with data exactly when the code carries data, parsed through
|
|
87
|
+
* the code's schema as an output is: undeclared fields stripped, defaults
|
|
88
|
+
* filled, transforms run. The parse is synchronous, so a data schema cannot
|
|
89
|
+
* be async.
|
|
90
|
+
* - Anything else is refused, a declared code raised with a status or flag of
|
|
91
|
+
* its own included.
|
|
92
|
+
*
|
|
93
|
+
* `endpoint` undefined means the endpoint's codes are not known here (a mock
|
|
94
|
+
* given no generated options), and nothing is checked.
|
|
95
|
+
*/
|
|
96
|
+
export const checkedRefusal = (apiName, endpoint, thrown) => {
|
|
97
|
+
if (endpoint === undefined)
|
|
98
|
+
return thrown;
|
|
99
|
+
const { code, data } = thrown.refusal;
|
|
100
|
+
if (code === undefined || isLambderRefusalCode(code)) {
|
|
101
|
+
if (code === undefined && endpoint.codeRequired)
|
|
102
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { uncoded: true });
|
|
103
|
+
if (data !== undefined)
|
|
104
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { dataWithoutDeclaredCode: code });
|
|
105
|
+
return thrown;
|
|
106
|
+
}
|
|
107
|
+
const declaration = endpoint.codes.get(code);
|
|
108
|
+
if (declaration === undefined)
|
|
109
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { undeclared: code });
|
|
110
|
+
if (thrown.statusCode !== undefined || thrown.notAuthorized || thrown.sessionExpired) {
|
|
111
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { ownStatusOrFlag: true });
|
|
112
|
+
}
|
|
113
|
+
let sent;
|
|
114
|
+
if (!declaration.data) {
|
|
115
|
+
if (data !== undefined)
|
|
116
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { dataWithoutDeclaredCode: code });
|
|
117
|
+
}
|
|
118
|
+
else if (data === undefined) {
|
|
119
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { missingData: code });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = declaration.schema.safeParse(data);
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { thrown: cause });
|
|
128
|
+
}
|
|
129
|
+
if (!parsed.success)
|
|
130
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { zodError: parsed.error });
|
|
131
|
+
if (!isObjectPayload(parsed.data))
|
|
132
|
+
throw new LambderApiRefusalValidationError(apiName, thrown, { notObject: describePayloadKind(parsed.data) });
|
|
133
|
+
sent = parsed.data;
|
|
134
|
+
}
|
|
135
|
+
// The declaration's status and flag, and the data as parsed: how every
|
|
136
|
+
// refusal with this code leaves, wherever it was raised.
|
|
137
|
+
return new LambderApiRefusal(thrown.message, {
|
|
138
|
+
refusal: { ...thrown.refusal, ...(sent !== undefined ? { data: sent } : {}) },
|
|
139
|
+
notAuthorized: declaration.notAuthorized,
|
|
140
|
+
statusCode: declaration.status,
|
|
141
|
+
headers: thrown.headers,
|
|
142
|
+
cause: thrown.cause,
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Throws when `where` (an API, a guard) names a refusal code the vocabulary
|
|
147
|
+
* does not hold, or names one when there is no vocabulary at all.
|
|
148
|
+
*/
|
|
149
|
+
export const assertRefusalCodesDeclared = (where, codes, vocabulary) => {
|
|
150
|
+
for (const code of codes) {
|
|
151
|
+
if (!vocabulary) {
|
|
152
|
+
throw new Error(`Lambder: ${where} declares the refusal "${code}", but no refusals vocabulary was declared. Declare the app's codes with initLambder().declareRefusals().`);
|
|
153
|
+
}
|
|
154
|
+
if (!vocabulary.has(code)) {
|
|
155
|
+
throw new Error(`Lambder: ${where} declares the refusal "${code}", which the refusals vocabulary does not hold.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* One endpoint's allowed codes, resolved from the vocabulary: the codes its
|
|
161
|
+
* own `refusals` option names and those of its declared guards, each as
|
|
162
|
+
* checkedRefusal reads it, schema included. Throws on a name the vocabulary
|
|
163
|
+
* does not hold, and on a declaration when there is no vocabulary at all.
|
|
164
|
+
*/
|
|
165
|
+
export const resolveAllowedRefusals = (apiName, vocabulary, ownCodes, guardCodes) => {
|
|
166
|
+
const allowed = new Map();
|
|
167
|
+
const take = (code, where) => {
|
|
168
|
+
assertRefusalCodesDeclared(where, [code], vocabulary);
|
|
169
|
+
allowed.set(code, allowedRefusalOf(vocabulary.get(code)));
|
|
170
|
+
};
|
|
171
|
+
for (const code of ownCodes)
|
|
172
|
+
take(code, `API "${apiName}"`);
|
|
173
|
+
for (const { guard, codes } of guardCodes) {
|
|
174
|
+
for (const code of codes)
|
|
175
|
+
take(code, `guard "${guard}" (declared by API "${apiName}")`);
|
|
176
|
+
}
|
|
177
|
+
return allowed;
|
|
178
|
+
};
|
|
179
|
+
/** The refusals option in its list form: one name becomes a list of one. */
|
|
180
|
+
export const toRefusalCodes = (value) => value === undefined ? [] : typeof value === "string" ? [value] : value;
|
|
181
|
+
/**
|
|
182
|
+
* The vocabulary, checked: every code a non-empty string outside the
|
|
183
|
+
* framework's `lambder/` prefix, every declaration an object whose `data`,
|
|
184
|
+
* when present, is a zod schema, whose `status`, when present, is one a
|
|
185
|
+
* reader files as a refusal, and whose `notAuthorized`, when present, is
|
|
186
|
+
* true. A Map, so a code named for something Object.prototype carries
|
|
187
|
+
* ("toString") is still an ordinary code.
|
|
188
|
+
*/
|
|
189
|
+
export const readRefusalVocabulary = (refusals) => {
|
|
190
|
+
if (refusals === undefined)
|
|
191
|
+
return null;
|
|
192
|
+
const entries = Object.entries(refusals);
|
|
193
|
+
if (entries.length === 0) {
|
|
194
|
+
throw new Error("Lambder: the refusals vocabulary was declared with no codes in it, which declares nothing. Name the codes APIs will refuse with, or leave declareRefusals() out.");
|
|
195
|
+
}
|
|
196
|
+
for (const [code, declaration] of entries) {
|
|
197
|
+
if (code === "")
|
|
198
|
+
throw new Error("Lambder: the refusals vocabulary declares an empty code.");
|
|
199
|
+
if (code.startsWith("lambder/")) {
|
|
200
|
+
throw new Error(`Lambder: the refusals vocabulary declares "${code}", under the "lambder/" prefix the framework's own codes use. Name it outside that prefix.`);
|
|
201
|
+
}
|
|
202
|
+
if (declaration === null || typeof declaration !== "object") {
|
|
203
|
+
throw new Error(`Lambder: the refusal "${code}" is not declared as an object: write {} for a code with no data, or { data: schema }.`);
|
|
204
|
+
}
|
|
205
|
+
const extra = Object.keys(declaration).filter((key) => key !== "data" && key !== "status" && key !== "notAuthorized");
|
|
206
|
+
if (extra.length) {
|
|
207
|
+
throw new Error(`Lambder: the refusal "${code}" has ${extra.map((key) => `"${key}"`).join(", ")} beside data, status and notAuthorized, which a refusal declaration does not take.`);
|
|
208
|
+
}
|
|
209
|
+
// Read for what it is used as rather than by class, so a schema from
|
|
210
|
+
// a second copy of zod in the bundle still counts.
|
|
211
|
+
if (declaration.data !== undefined && typeof declaration.data?.safeParse !== "function") {
|
|
212
|
+
throw new Error(`Lambder: the refusal "${code}" declares data that is not a zod schema.`);
|
|
213
|
+
}
|
|
214
|
+
const status = declaration.status;
|
|
215
|
+
if (status !== undefined && (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status >= 500 || status === 422)) {
|
|
216
|
+
throw new Error(`Lambder: the refusal "${code}" declares the status ${String(status)}. A declared refusal leaves with a status from 200 to 499 other than 422: a reader files a 5xx as a server failure and a 422 as an input validation failure.`);
|
|
217
|
+
}
|
|
218
|
+
if (declaration.notAuthorized !== undefined && declaration.notAuthorized !== true) {
|
|
219
|
+
throw new Error(`Lambder: the refusal "${code}" declares notAuthorized as ${String(declaration.notAuthorized)}; write true, or leave it off.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return new Map(entries);
|
|
223
|
+
};
|
|
@@ -16,10 +16,19 @@ export type LambderApiSignatureEntry = {
|
|
|
16
16
|
* The digest of an endpoint's client-facing shape: its name and mode, its
|
|
17
17
|
* input and output schemas as JSON Schema, every guard it declares with the
|
|
18
18
|
* schema that guard validates (the guardInput the client sends separately,
|
|
19
|
-
* or the apiInput slice of the payload),
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* or the apiInput slice of the payload), whether it demands an idempotency
|
|
20
|
+
* key, and every refusal code it may send with its data's schema, its status
|
|
21
|
+
* and its notAuthorized flag. Anything
|
|
22
|
+
* else (rate limits, a guard's parameter, the handler) changes nothing for a
|
|
23
|
+
* client and is left out, so changing it never forces a reload.
|
|
24
|
+
*
|
|
25
|
+
* The refusal codes count in full: a client's refusal type lists exactly the
|
|
26
|
+
* codes the endpoint declares, so one built before a code was added would be
|
|
27
|
+
* handed a code its types say cannot arrive. Adding a code, or changing a
|
|
28
|
+
* code's data, reloads that endpoint's clients. A code's data is received,
|
|
29
|
+
* so it is digested in the output position, where an extensibleEnum's values
|
|
30
|
+
* leave the digest. Its status and flag count too: a caller reads the one
|
|
31
|
+
* and routes on the other.
|
|
23
32
|
*
|
|
24
33
|
* Schemas are hashed as built, descriptions and titles included, so a client
|
|
25
34
|
* built against a different one reloads once. The exception is an output
|
|
@@ -54,10 +54,19 @@ const ownGuard = (guards, name) => guards !== undefined && Object.prototype.hasO
|
|
|
54
54
|
* The digest of an endpoint's client-facing shape: its name and mode, its
|
|
55
55
|
* input and output schemas as JSON Schema, every guard it declares with the
|
|
56
56
|
* schema that guard validates (the guardInput the client sends separately,
|
|
57
|
-
* or the apiInput slice of the payload),
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* or the apiInput slice of the payload), whether it demands an idempotency
|
|
58
|
+
* key, and every refusal code it may send with its data's schema, its status
|
|
59
|
+
* and its notAuthorized flag. Anything
|
|
60
|
+
* else (rate limits, a guard's parameter, the handler) changes nothing for a
|
|
61
|
+
* client and is left out, so changing it never forces a reload.
|
|
62
|
+
*
|
|
63
|
+
* The refusal codes count in full: a client's refusal type lists exactly the
|
|
64
|
+
* codes the endpoint declares, so one built before a code was added would be
|
|
65
|
+
* handed a code its types say cannot arrive. Adding a code, or changing a
|
|
66
|
+
* code's data, reloads that endpoint's clients. A code's data is received,
|
|
67
|
+
* so it is digested in the output position, where an extensibleEnum's values
|
|
68
|
+
* leave the digest. Its status and flag count too: a caller reads the one
|
|
69
|
+
* and routes on the other.
|
|
61
70
|
*
|
|
62
71
|
* Schemas are hashed as built, descriptions and titles included, so a client
|
|
63
72
|
* built against a different one reloads once. The exception is an output
|
|
@@ -75,6 +84,13 @@ export const apiSignatureOf = async (definition, guards) => {
|
|
|
75
84
|
}];
|
|
76
85
|
});
|
|
77
86
|
guardShapes.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
87
|
+
const refusalShapes = [...(definition.refusals?.codes ?? new Map()).entries()]
|
|
88
|
+
.map(([code, declaration]) => [code, {
|
|
89
|
+
data: jsonSchemaOf(declaration.data ? declaration.schema : undefined, "output"),
|
|
90
|
+
status: declaration.status ?? 200,
|
|
91
|
+
notAuthorized: declaration.notAuthorized === true,
|
|
92
|
+
}]);
|
|
93
|
+
refusalShapes.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
78
94
|
const description = {
|
|
79
95
|
name: definition.name,
|
|
80
96
|
mode: definition.mode,
|
|
@@ -82,6 +98,7 @@ export const apiSignatureOf = async (definition, guards) => {
|
|
|
82
98
|
output: jsonSchemaOf(definition.output, "output"),
|
|
83
99
|
guards: guardShapes,
|
|
84
100
|
idempotency: definition.idempotency !== undefined && definition.idempotency !== false,
|
|
101
|
+
refusals: refusalShapes,
|
|
85
102
|
};
|
|
86
103
|
const hex = await sha256HexOf(canonicalJson(description));
|
|
87
104
|
return hex.slice(0, API_SIGNATURE_HEX_LENGTH);
|
|
@@ -3,9 +3,11 @@ import { resolve } from "path";
|
|
|
3
3
|
import { headerLinesOf, loadApiOptionEntries, nameChangesOf, readGeneratedTable, settleGeneratedFile, tableStatementLines, } from "./generatedTables.js";
|
|
4
4
|
/*
|
|
5
5
|
* The declared options of an app's APIs as a generated module of plain data:
|
|
6
|
-
* every API's mode and its guards, rateLimit
|
|
7
|
-
* written, every rate-limit policy less its key handler, and
|
|
8
|
-
* input mode, session flag
|
|
6
|
+
* every API's mode and its guards, rateLimit, idempotency and refusals
|
|
7
|
+
* options as written, every rate-limit policy less its key handler, and
|
|
8
|
+
* every guard's input mode, session flag, place in the call and refusal
|
|
9
|
+
* codes. The refusal vocabulary itself is not written: it is shared code,
|
|
10
|
+
* which the mock declares as the server does. The contract already
|
|
9
11
|
* carries the options as types; this is the same fact as a value, for the
|
|
10
12
|
* code that decides something at runtime with it: a mock restating the
|
|
11
13
|
* server's declarations, a test walking the public surface. Neither imports
|
|
@@ -29,9 +31,9 @@ const DEFAULT_HEADER = [
|
|
|
29
31
|
].join("\n");
|
|
30
32
|
/** The three tables the module exports, in the order they are written. */
|
|
31
33
|
const TABLES = [
|
|
32
|
-
{ name: "apiOptions", key: "apis", type: "LambderApiOptionEntry", doc: "Every API the server registers: its mode and its guards, rateLimit and
|
|
34
|
+
{ name: "apiOptions", key: "apis", type: "LambderApiOptionEntry", doc: "Every API the server registers: its mode and its guards, rateLimit, idempotency and refusals options as written." },
|
|
33
35
|
{ name: "rateLimitPolicies", key: "rateLimitPolicies", type: "LambderRateLimitPolicyEntry", doc: "Every rate-limit policy the server declares, its key reduced to what it counts: an address, a session, or a key the app derives (\"custom\")." },
|
|
34
|
-
{ name: "guardDeclarations", key: "guards", type: "LambderGuardDeclarationEntry", doc: "Every guard the server declares: how it is fed, whether it needs a session, and
|
|
36
|
+
{ name: "guardDeclarations", key: "guards", type: "LambderGuardDeclarationEntry", doc: "Every guard the server declares: how it is fed, whether it needs a session, when it runs, and the refusal codes it adds to the APIs declaring it." },
|
|
35
37
|
];
|
|
36
38
|
/** The tables a generated file holds, read back as data, or null for a file that does not hold all three as written (never written, or rewritten by hand). */
|
|
37
39
|
export const readOptionTables = (contents) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LambderUncheckedRefusalMessage } from '../shared/wire/LambderApiRefusal.js';
|
|
2
2
|
import { type LambderRequestCompressionOption } from '../shared/wire/LambderRequestPayload.js';
|
|
3
|
-
import type { LambderApiContractShape } from '../shared/wire/LambderApiContract.js';
|
|
3
|
+
import type { LambderApiContractShape, LambderContractAnyRefusalMessage, LambderContractRefusalMessage } from '../shared/wire/LambderApiContract.js';
|
|
4
4
|
import { type LambderApiOutcome, type LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
|
|
5
5
|
import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from '../shared/wire/LambderCallOptions.js';
|
|
6
6
|
import { type LambderApiTransport } from '../shared/transport/LambderApiTransport.js';
|
|
@@ -29,18 +29,25 @@ type FetchEndEventHandler = (params: {
|
|
|
29
29
|
}) => void | Promise<void>;
|
|
30
30
|
type ErrorHandler = (err: Error) => void | Promise<void>;
|
|
31
31
|
type ValidationErrorHandler = (zodError: LambderValidationError) => (void | false) | Promise<(void | false)>;
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Handed the refusal as its message object, a plain-string refusal
|
|
34
|
+
* having been read as one (refusalMessageOf). TMessage is what the calls it
|
|
35
|
+
* hears can refuse with: one endpoint's declared codes on a per-call
|
|
36
|
+
* override, every endpoint's on the constructor's.
|
|
37
|
+
*/
|
|
38
|
+
type RefusalHandler<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = (message: TMessage) => void | Promise<void>;
|
|
34
39
|
/** The logListHandler option: an answer's logList, success or failure, when it has entries. The invoke caller's onLogList, for a browser. */
|
|
35
40
|
export type LambderLogListHandler = (apiName: string, logList: unknown[]) => void | Promise<void>;
|
|
36
41
|
/**
|
|
37
42
|
* Per-call options: the request extras both callers share (see
|
|
38
43
|
* LambderSharedCallOptions) plus an override for every constructor handler.
|
|
44
|
+
* TMessage is the endpoint's refusal message, which the per-call
|
|
45
|
+
* refusalHandler is handed.
|
|
39
46
|
*/
|
|
40
|
-
export type LambderCallOptions = LambderSharedCallOptions & {
|
|
47
|
+
export type LambderCallOptions<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderSharedCallOptions & {
|
|
41
48
|
versionExpiredHandler?: NotifyHandler;
|
|
42
49
|
sessionExpiredHandler?: NotifyHandler;
|
|
43
|
-
|
|
50
|
+
refusalHandler?: RefusalHandler<TMessage>;
|
|
44
51
|
apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
45
52
|
notAuthorizedHandler?: NotifyHandler;
|
|
46
53
|
errorHandler?: ErrorHandler;
|
|
@@ -48,7 +55,7 @@ export type LambderCallOptions = LambderSharedCallOptions & {
|
|
|
48
55
|
fetchStartedHandler?: FetchStartEventHandler;
|
|
49
56
|
fetchEndedHandler?: FetchEndEventHandler;
|
|
50
57
|
};
|
|
51
|
-
type LambderCallerBaseOptions = {
|
|
58
|
+
type LambderCallerBaseOptions<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = {
|
|
52
59
|
apiPath: string;
|
|
53
60
|
/** Sent with every call as `version`, informational: the server stamps its own on every answer. */
|
|
54
61
|
apiVersion?: string;
|
|
@@ -71,7 +78,8 @@ type LambderCallerBaseOptions = {
|
|
|
71
78
|
timeoutMs?: number;
|
|
72
79
|
versionExpiredHandler?: NotifyHandler;
|
|
73
80
|
sessionExpiredHandler?: NotifyHandler;
|
|
74
|
-
|
|
81
|
+
/** Handed every refusal a call of this caller comes back with, typed with every code the contract declares. */
|
|
82
|
+
refusalHandler?: RefusalHandler<TMessage>;
|
|
75
83
|
notAuthorizedHandler?: NotifyHandler;
|
|
76
84
|
errorHandler?: ErrorHandler;
|
|
77
85
|
/** Receives each answer's logList, with the API name. Default: console.log with a `[lambder]` prefix, one line per entry. */
|
|
@@ -100,7 +108,7 @@ type LambderCallerBaseOptions = {
|
|
|
100
108
|
transport?: LambderApiTransport;
|
|
101
109
|
};
|
|
102
110
|
/** Constructor options: the base options plus guardInputsProvider, mandatory once TProvided names guards. */
|
|
103
|
-
export type LambderCallerOptions<TContract, TProvided extends string = never> = LambderCallerBaseOptions & LambderGuardInputsProviderOption<TContract, TProvided>;
|
|
111
|
+
export type LambderCallerOptions<TContract, TProvided extends string = never> = LambderCallerBaseOptions<LambderContractAnyRefusalMessage<TContract>> & LambderGuardInputsProviderOption<TContract, TProvided>;
|
|
104
112
|
/**
|
|
105
113
|
* @typeParam TContract - The API contract, for typed names, payloads and guard inputs.
|
|
106
114
|
* @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
|
|
@@ -116,7 +124,7 @@ export default class LambderCaller<TContract extends LambderApiContractShape = a
|
|
|
116
124
|
get isLoading(): boolean;
|
|
117
125
|
private versionExpiredHandler?;
|
|
118
126
|
private sessionExpiredHandler?;
|
|
119
|
-
private
|
|
127
|
+
private refusalHandler?;
|
|
120
128
|
private notAuthorizedHandler?;
|
|
121
129
|
private errorHandler?;
|
|
122
130
|
private apiInputValidationErrorHandler?;
|
|
@@ -138,18 +146,21 @@ export default class LambderCaller<TContract extends LambderApiContractShape = a
|
|
|
138
146
|
private dispatch;
|
|
139
147
|
/**
|
|
140
148
|
* Full-fidelity call: resolves to a discriminated LambderApiOutcome
|
|
141
|
-
* instead of collapsing every failure to undefined. Never throws.
|
|
149
|
+
* instead of collapsing every failure to undefined. Never throws. A
|
|
150
|
+
* success's payload is the endpoint's output, and a refusal's
|
|
151
|
+
* refusal narrows on the codes the endpoint declares.
|
|
142
152
|
*
|
|
143
153
|
* The output is computed from the contract in the return type rather than
|
|
144
154
|
* taken as a type parameter, so a call site cannot replace it by
|
|
145
155
|
* annotating what it assigns to.
|
|
146
156
|
*/
|
|
147
|
-
apiOutcome<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions
|
|
157
|
+
apiOutcome<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions<LambderContractRefusalMessage<TContract, TApiName>>>): Promise<LambderApiOutcome<LambderContractOutputOf<TContract, TApiName>, LambderContractRefusalMessage<TContract, TApiName>>>;
|
|
148
158
|
/**
|
|
149
|
-
* The
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
159
|
+
* The endpoint's output on success, `undefined` on every failure. An
|
|
160
|
+
* output is always an object or an array, so the result is truthy exactly
|
|
161
|
+
* when the call succeeded; the handlers configured on the caller have
|
|
162
|
+
* already been told why it did not. Use apiOutcome() to branch on the
|
|
163
|
+
* reason at the call site.
|
|
153
164
|
*/
|
|
154
|
-
api<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions
|
|
165
|
+
api<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions<LambderContractRefusalMessage<TContract, TApiName>>>): Promise<LambderContractOutputOf<TContract, TApiName> | undefined>;
|
|
155
166
|
}
|