counterfact 2.6.0 → 2.7.0
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/README.md +103 -141
- package/bin/README.md +24 -4
- package/bin/counterfact.js +44 -1
- package/dist/app.js +15 -16
- package/dist/counterfact-types/cookie-options.js +1 -0
- package/dist/counterfact-types/counterfact-response.js +7 -0
- package/dist/counterfact-types/example-names.js +1 -0
- package/dist/counterfact-types/example.js +1 -0
- package/dist/counterfact-types/generic-response-builder.js +1 -0
- package/dist/counterfact-types/http-status-code.js +1 -0
- package/dist/counterfact-types/if-has-key.js +1 -0
- package/dist/counterfact-types/index.js +0 -1
- package/dist/counterfact-types/maybe-promise.js +1 -0
- package/dist/counterfact-types/media-type.js +1 -0
- package/dist/counterfact-types/omit-all.js +1 -0
- package/dist/counterfact-types/omit-value-when-never.js +1 -0
- package/dist/counterfact-types/open-api-content.js +1 -0
- package/dist/counterfact-types/open-api-operation.js +1 -0
- package/dist/counterfact-types/open-api-parameters.js +1 -0
- package/dist/counterfact-types/open-api-response.js +1 -0
- package/dist/counterfact-types/random-function.js +1 -0
- package/dist/counterfact-types/response-builder-factory.js +1 -0
- package/dist/counterfact-types/response-builder.js +1 -0
- package/dist/counterfact-types/wide-operation-argument.js +1 -0
- package/dist/counterfact-types/wide-response-builder.js +1 -0
- package/dist/repl/repl.js +96 -3
- package/dist/server/context-registry.js +17 -1
- package/dist/server/counterfact-types/cookie-options.ts +14 -0
- package/dist/server/counterfact-types/counterfact-response.ts +15 -0
- package/dist/server/counterfact-types/example-names.ts +13 -0
- package/dist/server/counterfact-types/example.ts +10 -0
- package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
- package/dist/server/counterfact-types/http-status-code.ts +62 -0
- package/dist/server/counterfact-types/if-has-key.ts +19 -0
- package/dist/server/counterfact-types/index.ts +20 -338
- package/dist/server/counterfact-types/maybe-promise.ts +6 -0
- package/dist/server/counterfact-types/media-type.ts +6 -0
- package/dist/server/counterfact-types/omit-all.ts +11 -0
- package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
- package/dist/server/counterfact-types/open-api-content.ts +8 -0
- package/dist/server/counterfact-types/open-api-operation.ts +36 -0
- package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
- package/dist/server/counterfact-types/open-api-response.ts +22 -0
- package/dist/server/counterfact-types/random-function.ts +9 -0
- package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
- package/dist/server/counterfact-types/response-builder.ts +31 -0
- package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
- package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
- package/dist/server/create-koa-app.js +1 -20
- package/dist/server/dispatcher.js +18 -5
- package/dist/server/json-to-xml.js +1 -1
- package/dist/server/koa-middleware.js +7 -1
- package/dist/server/load-openapi-document.js +13 -0
- package/dist/server/module-loader.js +76 -4
- package/dist/server/openapi-watcher.js +35 -0
- package/dist/server/request-validator.js +3 -7
- package/dist/server/response-builder.js +3 -0
- package/dist/server/response-validator.js +58 -0
- package/dist/server/scenario-registry.js +29 -0
- package/dist/server/tools.js +2 -2
- package/dist/typescript-generator/coder.js +4 -2
- package/dist/typescript-generator/generate.js +155 -0
- package/dist/typescript-generator/operation-coder.js +1 -1
- package/dist/typescript-generator/operation-type-coder.js +1 -49
- package/dist/typescript-generator/read-only-comments.js +1 -1
- package/dist/typescript-generator/requirement.js +8 -1
- package/dist/typescript-generator/reserved-words.js +50 -0
- package/dist/util/load-config-file.js +44 -0
- package/package.json +7 -8
- package/dist/client/README.md +0 -14
- package/dist/client/index.html.hbs +0 -244
- package/dist/client/rapi-doc.html.hbs +0 -36
- package/dist/server/page-middleware.js +0 -23
|
@@ -1,339 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
schema: unknown;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface Example {
|
|
8
|
-
description: string;
|
|
9
|
-
summary: string;
|
|
10
|
-
value: unknown;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface CookieOptions {
|
|
14
|
-
domain?: string;
|
|
15
|
-
expires?: Date;
|
|
16
|
-
httpOnly?: boolean;
|
|
17
|
-
maxAge?: number;
|
|
18
|
-
path?: string;
|
|
19
|
-
sameSite?: "lax" | "none" | "strict";
|
|
20
|
-
secure?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const counterfactResponse = Symbol("Counterfact Response");
|
|
24
|
-
|
|
25
|
-
type COUNTERFACT_RESPONSE = {
|
|
26
|
-
[counterfactResponse]: typeof counterfactResponse;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type MediaType = `${string}/${string}`;
|
|
30
|
-
|
|
31
|
-
type MaybePromise<T> = T | Promise<T>;
|
|
32
|
-
|
|
33
|
-
type OmitAll<T, K extends readonly string[]> = {
|
|
34
|
-
[P in keyof T as P extends `${string}${K[number]}${string}`
|
|
35
|
-
? never
|
|
36
|
-
: P]: T[P];
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type OmitValueWhenNever<Base> = Pick<
|
|
40
|
-
Base,
|
|
41
|
-
{
|
|
42
|
-
[Key in keyof Base]: [Base[Key]] extends [never] ? never : Key;
|
|
43
|
-
}[keyof Base]
|
|
44
|
-
>;
|
|
45
|
-
|
|
46
|
-
interface OpenApiResponse {
|
|
47
|
-
content: { [key: MediaType]: OpenApiContent };
|
|
48
|
-
examples?: { [key: string]: unknown };
|
|
49
|
-
headers: { [key: string]: { schema: unknown } };
|
|
50
|
-
requiredHeaders: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface OpenApiResponses {
|
|
54
|
-
[key: string]: OpenApiResponse;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type IfHasKey<
|
|
58
|
-
SomeObject,
|
|
59
|
-
Keys extends readonly string[],
|
|
60
|
-
Yes,
|
|
61
|
-
No,
|
|
62
|
-
> = Keys extends [
|
|
63
|
-
infer FirstKey extends string,
|
|
64
|
-
...infer RestKeys extends string[],
|
|
65
|
-
]
|
|
66
|
-
? Extract<keyof SomeObject, `${string}${FirstKey}${string}`> extends never
|
|
67
|
-
? IfHasKey<SomeObject, RestKeys, Yes, No>
|
|
68
|
-
: Yes
|
|
69
|
-
: No;
|
|
70
|
-
|
|
71
|
-
type SchemasOf<T extends { [key: string]: { schema: unknown } }> = {
|
|
72
|
-
[K in keyof T]: T[K]["schema"];
|
|
73
|
-
}[keyof T];
|
|
74
|
-
|
|
75
|
-
type MaybeShortcut<
|
|
76
|
-
ContentTypes extends MediaType[],
|
|
77
|
-
Response extends OpenApiResponse,
|
|
78
|
-
> = IfHasKey<
|
|
79
|
-
Response["content"],
|
|
80
|
-
ContentTypes,
|
|
81
|
-
(body: SchemasOf<Response["content"]>) => GenericResponseBuilder<{
|
|
82
|
-
content: NeverIfEmpty<OmitAll<Response["content"], ContentTypes>>;
|
|
83
|
-
headers: Response["headers"];
|
|
84
|
-
requiredHeaders: Response["requiredHeaders"];
|
|
85
|
-
}>,
|
|
86
|
-
never
|
|
87
|
-
>;
|
|
88
|
-
|
|
89
|
-
type NeverIfEmpty<Record> = object extends Record ? never : Record;
|
|
90
|
-
|
|
91
|
-
type MatchFunction<Response extends OpenApiResponse> = <
|
|
92
|
-
ContentType extends MediaType & keyof Response["content"],
|
|
93
|
-
>(
|
|
94
|
-
contentType: ContentType,
|
|
95
|
-
body: Response["content"][ContentType]["schema"],
|
|
96
|
-
) => GenericResponseBuilder<{
|
|
97
|
-
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
|
|
98
|
-
headers: Response["headers"];
|
|
99
|
-
requiredHeaders: Response["requiredHeaders"];
|
|
100
|
-
}>;
|
|
101
|
-
|
|
102
|
-
type HeaderFunction<Response extends OpenApiResponse> = <
|
|
103
|
-
Header extends string & keyof Response["headers"],
|
|
104
|
-
>(
|
|
105
|
-
header: Header,
|
|
106
|
-
value: Response["headers"][Header]["schema"],
|
|
107
|
-
) => GenericResponseBuilder<{
|
|
108
|
-
content: NeverIfEmpty<Response["content"]>;
|
|
109
|
-
headers: NeverIfEmpty<Omit<Response["headers"], Header>>;
|
|
110
|
-
requiredHeaders: Exclude<Response["requiredHeaders"], Header>;
|
|
111
|
-
}>;
|
|
112
|
-
|
|
113
|
-
type RandomFunction = () => MaybePromise<COUNTERFACT_RESPONSE>;
|
|
114
|
-
|
|
115
|
-
type ExampleNames<Response extends OpenApiResponse> = Response extends {
|
|
116
|
-
examples: infer E;
|
|
117
|
-
}
|
|
118
|
-
? keyof E & string
|
|
119
|
-
: never;
|
|
120
|
-
|
|
121
|
-
interface ResponseBuilder {
|
|
122
|
-
[status: number | `${number} ${string}`]: ResponseBuilder;
|
|
123
|
-
binary: (body: Uint8Array | string) => ResponseBuilder;
|
|
124
|
-
content?: { body: unknown; type: string }[];
|
|
125
|
-
cookie: (
|
|
126
|
-
name: string,
|
|
127
|
-
value: string,
|
|
128
|
-
options?: CookieOptions,
|
|
129
|
-
) => ResponseBuilder;
|
|
130
|
-
example: (name: string) => ResponseBuilder;
|
|
131
|
-
header: (name: string, value: string) => ResponseBuilder;
|
|
132
|
-
headers: { [name: string]: string | string[] };
|
|
133
|
-
html: (body: unknown) => ResponseBuilder;
|
|
134
|
-
json: (body: unknown) => ResponseBuilder;
|
|
135
|
-
match: (contentType: string, body: unknown) => ResponseBuilder;
|
|
136
|
-
random: () => MaybePromise<ResponseBuilder>;
|
|
137
|
-
randomLegacy: () => MaybePromise<ResponseBuilder>;
|
|
138
|
-
status?: number;
|
|
139
|
-
text: (body: unknown) => ResponseBuilder;
|
|
140
|
-
xml: (body: unknown) => ResponseBuilder;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export type GenericResponseBuilderInner<
|
|
144
|
-
Response extends OpenApiResponse = OpenApiResponse,
|
|
145
|
-
> = OmitValueWhenNever<{
|
|
146
|
-
binary: MaybeShortcut<["application/octet-stream"], Response>;
|
|
147
|
-
cookie: (
|
|
148
|
-
name: string,
|
|
149
|
-
value: string,
|
|
150
|
-
options?: CookieOptions,
|
|
151
|
-
) => GenericResponseBuilder<Response>;
|
|
152
|
-
header: [keyof Response["headers"]] extends [never]
|
|
153
|
-
? never
|
|
154
|
-
: HeaderFunction<Response>;
|
|
155
|
-
html: MaybeShortcut<["text/html"], Response>;
|
|
156
|
-
json: MaybeShortcut<
|
|
157
|
-
[
|
|
158
|
-
"application/json",
|
|
159
|
-
"text/json",
|
|
160
|
-
"text/x-json",
|
|
161
|
-
"application/xml",
|
|
162
|
-
"text/xml",
|
|
163
|
-
],
|
|
164
|
-
Response
|
|
165
|
-
>;
|
|
166
|
-
match: [keyof Response["content"]] extends [never]
|
|
167
|
-
? never
|
|
168
|
-
: MatchFunction<Response>;
|
|
169
|
-
random: [keyof Response["content"]] extends [never] ? never : RandomFunction;
|
|
170
|
-
example: [ExampleNames<Response>] extends [never]
|
|
171
|
-
? never
|
|
172
|
-
: (name: ExampleNames<Response>) => COUNTERFACT_RESPONSE;
|
|
173
|
-
text: MaybeShortcut<["text/plain"], Response>;
|
|
174
|
-
xml: MaybeShortcut<["application/xml", "text/xml"], Response>;
|
|
175
|
-
}>;
|
|
176
|
-
|
|
177
|
-
type GenericResponseBuilder<
|
|
178
|
-
Response extends OpenApiResponse = OpenApiResponse,
|
|
179
|
-
> =
|
|
180
|
-
object extends OmitValueWhenNever<Response>
|
|
181
|
-
? COUNTERFACT_RESPONSE
|
|
182
|
-
: keyof OmitValueWhenNever<Response> extends "headers"
|
|
183
|
-
? {
|
|
184
|
-
ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE;
|
|
185
|
-
header: HeaderFunction<Response>;
|
|
186
|
-
}
|
|
187
|
-
: GenericResponseBuilderInner<Response>;
|
|
188
|
-
|
|
189
|
-
type ResponseBuilderFactory<
|
|
190
|
-
Responses extends OpenApiResponses = OpenApiResponses,
|
|
191
|
-
> = {
|
|
192
|
-
[StatusCode in keyof Responses]: GenericResponseBuilder<
|
|
193
|
-
Responses[StatusCode]
|
|
194
|
-
>;
|
|
195
|
-
} & { [key: string]: GenericResponseBuilder<Responses["default"]> };
|
|
196
|
-
|
|
197
|
-
type HttpStatusCode =
|
|
198
|
-
| 100
|
|
199
|
-
| 101
|
|
200
|
-
| 102
|
|
201
|
-
| 200
|
|
202
|
-
| 201
|
|
203
|
-
| 202
|
|
204
|
-
| 203
|
|
205
|
-
| 204
|
|
206
|
-
| 205
|
|
207
|
-
| 206
|
|
208
|
-
| 207
|
|
209
|
-
| 226
|
|
210
|
-
| 300
|
|
211
|
-
| 301
|
|
212
|
-
| 302
|
|
213
|
-
| 303
|
|
214
|
-
| 304
|
|
215
|
-
| 305
|
|
216
|
-
| 307
|
|
217
|
-
| 308
|
|
218
|
-
| 400
|
|
219
|
-
| 401
|
|
220
|
-
| 402
|
|
221
|
-
| 403
|
|
222
|
-
| 404
|
|
223
|
-
| 405
|
|
224
|
-
| 406
|
|
225
|
-
| 407
|
|
226
|
-
| 408
|
|
227
|
-
| 409
|
|
228
|
-
| 410
|
|
229
|
-
| 411
|
|
230
|
-
| 412
|
|
231
|
-
| 413
|
|
232
|
-
| 414
|
|
233
|
-
| 415
|
|
234
|
-
| 416
|
|
235
|
-
| 417
|
|
236
|
-
| 418
|
|
237
|
-
| 422
|
|
238
|
-
| 423
|
|
239
|
-
| 424
|
|
240
|
-
| 426
|
|
241
|
-
| 428
|
|
242
|
-
| 429
|
|
243
|
-
| 431
|
|
244
|
-
| 451
|
|
245
|
-
| 500
|
|
246
|
-
| 501
|
|
247
|
-
| 502
|
|
248
|
-
| 503
|
|
249
|
-
| 504
|
|
250
|
-
| 505
|
|
251
|
-
| 506
|
|
252
|
-
| 507
|
|
253
|
-
| 511;
|
|
254
|
-
|
|
255
|
-
interface OpenApiParameters {
|
|
256
|
-
in: "body" | "cookie" | "formData" | "header" | "path" | "query";
|
|
257
|
-
name: string;
|
|
258
|
-
required?: boolean;
|
|
259
|
-
schema?: {
|
|
260
|
-
[key: string]: unknown;
|
|
261
|
-
type?: string;
|
|
262
|
-
};
|
|
263
|
-
type?: "string" | "number" | "integer" | "boolean";
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
interface OpenApiOperation {
|
|
267
|
-
parameters?: OpenApiParameters[];
|
|
268
|
-
produces?: string[];
|
|
269
|
-
requestBody?: {
|
|
270
|
-
content?: {
|
|
271
|
-
[mediaType: string]: {
|
|
272
|
-
schema: { [key: string]: unknown };
|
|
273
|
-
};
|
|
274
|
-
};
|
|
275
|
-
required?: boolean;
|
|
276
|
-
};
|
|
277
|
-
responses: {
|
|
278
|
-
[status: string]: {
|
|
279
|
-
content?: {
|
|
280
|
-
[type: number | string]: {
|
|
281
|
-
examples?: { [key: string]: Example };
|
|
282
|
-
schema: { [key: string]: unknown };
|
|
283
|
-
};
|
|
284
|
-
};
|
|
285
|
-
examples?: { [key: string]: unknown };
|
|
286
|
-
headers?: {
|
|
287
|
-
[name: string]: OpenApiHeader;
|
|
288
|
-
};
|
|
289
|
-
schema?: { [key: string]: unknown };
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
interface WideResponseBuilder {
|
|
295
|
-
binary: (body: Uint8Array | string) => WideResponseBuilder;
|
|
296
|
-
example: (name: string) => WideResponseBuilder;
|
|
297
|
-
cookie: (
|
|
298
|
-
name: string,
|
|
299
|
-
value: string,
|
|
300
|
-
options?: CookieOptions,
|
|
301
|
-
) => WideResponseBuilder;
|
|
302
|
-
header: (body: unknown) => WideResponseBuilder;
|
|
303
|
-
html: (body: unknown) => WideResponseBuilder;
|
|
304
|
-
json: (body: unknown) => WideResponseBuilder;
|
|
305
|
-
match: (contentType: string, body: unknown) => WideResponseBuilder;
|
|
306
|
-
random: () => MaybePromise<WideResponseBuilder>;
|
|
307
|
-
text: (body: unknown) => WideResponseBuilder;
|
|
308
|
-
xml: (body: unknown) => WideResponseBuilder;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
interface WideOperationArgument {
|
|
312
|
-
body: unknown;
|
|
313
|
-
context: unknown;
|
|
314
|
-
headers: { [key: string]: string };
|
|
315
|
-
path: { [key: string]: string };
|
|
316
|
-
proxy: (url: string) => { proxyUrl: string };
|
|
317
|
-
query: { [key: string]: string };
|
|
318
|
-
response: { [key: number]: WideResponseBuilder };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export type { COUNTERFACT_RESPONSE };
|
|
322
|
-
|
|
1
|
+
export type { CookieOptions } from "./cookie-options.js";
|
|
2
|
+
export type { COUNTERFACT_RESPONSE } from "./counterfact-response.js";
|
|
3
|
+
export type { ExampleNames } from "./example-names.js";
|
|
323
4
|
export type {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
};
|
|
5
|
+
GenericResponseBuilder,
|
|
6
|
+
GenericResponseBuilderInner,
|
|
7
|
+
} from "./generic-response-builder.js";
|
|
8
|
+
export type { HttpStatusCode } from "./http-status-code.js";
|
|
9
|
+
export type { IfHasKey } from "./if-has-key.js";
|
|
10
|
+
export type { MaybePromise } from "./maybe-promise.js";
|
|
11
|
+
export type { MediaType } from "./media-type.js";
|
|
12
|
+
export type { OmitAll } from "./omit-all.js";
|
|
13
|
+
export type { OmitValueWhenNever } from "./omit-value-when-never.js";
|
|
14
|
+
export type { OpenApiHeader } from "./open-api-header.js";
|
|
15
|
+
export type { OpenApiOperation } from "./open-api-operation.js";
|
|
16
|
+
export type { OpenApiParameters } from "./open-api-parameters.js";
|
|
17
|
+
export type { OpenApiResponse } from "./open-api-response.js";
|
|
18
|
+
export type { ResponseBuilder } from "./response-builder.js";
|
|
19
|
+
export type { ResponseBuilderFactory } from "./response-builder-factory.js";
|
|
20
|
+
export type { WideOperationArgument } from "./wide-operation-argument.js";
|
|
21
|
+
export type { WideResponseBuilder } from "./wide-response-builder.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes all keys from `T` whose names contain any of the strings in `K`
|
|
3
|
+
* as a substring (prefix, suffix, or exact match).
|
|
4
|
+
* Used internally to narrow the set of available content-type methods on the
|
|
5
|
+
* response builder after one has already been called.
|
|
6
|
+
*/
|
|
7
|
+
export type OmitAll<T, K extends readonly string[]> = {
|
|
8
|
+
[P in keyof T as P extends `${string}${K[number]}${string}`
|
|
9
|
+
? never
|
|
10
|
+
: P]: T[P];
|
|
11
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a new type from `Base` that omits any keys whose value type is
|
|
3
|
+
* `never`. This is used to strip unavailable builder methods (those that
|
|
4
|
+
* don't apply to the current response shape) from the fluent response builder.
|
|
5
|
+
*/
|
|
6
|
+
export type OmitValueWhenNever<Base> = Pick<
|
|
7
|
+
Base,
|
|
8
|
+
{
|
|
9
|
+
[Key in keyof Base]: [Base[Key]] extends [never] ? never : Key;
|
|
10
|
+
}[keyof Base]
|
|
11
|
+
>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Example } from "./example.js";
|
|
2
|
+
import type { OpenApiHeader } from "./open-api-header.js";
|
|
3
|
+
import type { OpenApiParameters } from "./open-api-parameters.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Describes a single HTTP operation (e.g. `GET /pets`) as defined in an
|
|
7
|
+
* OpenAPI document. Used internally to derive the strongly-typed argument
|
|
8
|
+
* and response builder types for generated route handler functions.
|
|
9
|
+
*/
|
|
10
|
+
export interface OpenApiOperation {
|
|
11
|
+
parameters?: OpenApiParameters[];
|
|
12
|
+
produces?: string[];
|
|
13
|
+
requestBody?: {
|
|
14
|
+
content?: {
|
|
15
|
+
[mediaType: string]: {
|
|
16
|
+
schema: { [key: string]: unknown };
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
required?: boolean;
|
|
20
|
+
};
|
|
21
|
+
responses: {
|
|
22
|
+
[status: string]: {
|
|
23
|
+
content?: {
|
|
24
|
+
[type: number | string]: {
|
|
25
|
+
examples?: { [key: string]: Example };
|
|
26
|
+
schema: { [key: string]: unknown };
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
examples?: { [key: string]: unknown };
|
|
30
|
+
headers?: {
|
|
31
|
+
[name: string]: OpenApiHeader;
|
|
32
|
+
};
|
|
33
|
+
schema?: { [key: string]: unknown };
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describes a single parameter (path, query, header, cookie, body, or
|
|
3
|
+
* formData) as defined in an OpenAPI document. Used internally to type the
|
|
4
|
+
* `path`, `query`, `headers`, and `body` properties of a route handler's
|
|
5
|
+
* argument object.
|
|
6
|
+
*/
|
|
7
|
+
export interface OpenApiParameters {
|
|
8
|
+
in: "body" | "cookie" | "formData" | "header" | "path" | "query";
|
|
9
|
+
name: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
schema?: {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
type?: string;
|
|
14
|
+
};
|
|
15
|
+
type?: "string" | "number" | "integer" | "boolean";
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MediaType } from "./media-type.js";
|
|
2
|
+
import type { OpenApiContent } from "./open-api-content.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Describes a single HTTP response as modelled in an OpenAPI document.
|
|
6
|
+
* Contains the allowed content types, optional named examples, and the
|
|
7
|
+
* required/optional response headers for that response.
|
|
8
|
+
*/
|
|
9
|
+
export interface OpenApiResponse {
|
|
10
|
+
content: { [key: MediaType]: OpenApiContent };
|
|
11
|
+
examples?: { [key: string]: unknown };
|
|
12
|
+
headers: { [key: string]: { schema: unknown } };
|
|
13
|
+
requiredHeaders: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A map of HTTP status codes (or `"default"`) to their corresponding
|
|
18
|
+
* `OpenApiResponse` definitions for a given operation.
|
|
19
|
+
*/
|
|
20
|
+
export interface OpenApiResponses {
|
|
21
|
+
[key: string]: OpenApiResponse;
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js";
|
|
2
|
+
import type { MaybePromise } from "./maybe-promise.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The type of the `.random()` method on the response builder.
|
|
6
|
+
* When called, it randomly selects one of the available content-type examples
|
|
7
|
+
* and returns a completed `COUNTERFACT_RESPONSE`.
|
|
8
|
+
*/
|
|
9
|
+
export type RandomFunction = () => MaybePromise<COUNTERFACT_RESPONSE>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GenericResponseBuilder } from "./generic-response-builder.js";
|
|
2
|
+
import type { OpenApiResponses } from "./open-api-response.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps each HTTP status code (or `"default"`) in an OpenAPI operation's
|
|
6
|
+
* response definitions to the corresponding `GenericResponseBuilder`.
|
|
7
|
+
* This is the type of the `response` property in a generated route handler's
|
|
8
|
+
* argument object, allowing handlers to call e.g. `response[200].json(body)`.
|
|
9
|
+
*/
|
|
10
|
+
export type ResponseBuilderFactory<
|
|
11
|
+
Responses extends OpenApiResponses = OpenApiResponses,
|
|
12
|
+
> = {
|
|
13
|
+
[StatusCode in keyof Responses]: GenericResponseBuilder<
|
|
14
|
+
Responses[StatusCode]
|
|
15
|
+
>;
|
|
16
|
+
} & { [key: string]: GenericResponseBuilder<Responses["default"]> };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CookieOptions } from "./cookie-options.js";
|
|
2
|
+
import type { MaybePromise } from "./maybe-promise.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A loosely-typed, chainable response builder used in non-generated contexts
|
|
6
|
+
* (e.g. middleware or wide/catch-all route handlers) where the exact response
|
|
7
|
+
* shape is not statically known. For generated route handlers, prefer the
|
|
8
|
+
* strongly-typed `GenericResponseBuilder`.
|
|
9
|
+
*/
|
|
10
|
+
export interface ResponseBuilder {
|
|
11
|
+
[status: number | `${number} ${string}`]: ResponseBuilder;
|
|
12
|
+
binary: (body: Uint8Array | string) => ResponseBuilder;
|
|
13
|
+
content?: { body: unknown; type: string }[];
|
|
14
|
+
cookie: (
|
|
15
|
+
name: string,
|
|
16
|
+
value: string,
|
|
17
|
+
options?: CookieOptions,
|
|
18
|
+
) => ResponseBuilder;
|
|
19
|
+
empty: () => ResponseBuilder;
|
|
20
|
+
example: (name: string) => ResponseBuilder;
|
|
21
|
+
header: (name: string, value: string) => ResponseBuilder;
|
|
22
|
+
headers: { [name: string]: string | string[] };
|
|
23
|
+
html: (body: unknown) => ResponseBuilder;
|
|
24
|
+
json: (body: unknown) => ResponseBuilder;
|
|
25
|
+
match: (contentType: string, body: unknown) => ResponseBuilder;
|
|
26
|
+
random: () => MaybePromise<ResponseBuilder>;
|
|
27
|
+
randomLegacy: () => MaybePromise<ResponseBuilder>;
|
|
28
|
+
status?: number;
|
|
29
|
+
text: (body: unknown) => ResponseBuilder;
|
|
30
|
+
xml: (body: unknown) => ResponseBuilder;
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WideResponseBuilder } from "./wide-response-builder.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The loosely-typed argument object passed to wide (catch-all) route handlers.
|
|
5
|
+
* Unlike the generated operation argument types, all fields are typed as
|
|
6
|
+
* `unknown` or broad index signatures. Use this when writing handlers that
|
|
7
|
+
* should accept any request without compile-time schema enforcement.
|
|
8
|
+
*/
|
|
9
|
+
export interface WideOperationArgument {
|
|
10
|
+
body: unknown;
|
|
11
|
+
context: unknown;
|
|
12
|
+
headers: { [key: string]: string };
|
|
13
|
+
path: { [key: string]: string };
|
|
14
|
+
proxy: (url: string) => { proxyUrl: string };
|
|
15
|
+
query: { [key: string]: string };
|
|
16
|
+
response: { [key: number]: WideResponseBuilder };
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CookieOptions } from "./cookie-options.js";
|
|
2
|
+
import type { MaybePromise } from "./maybe-promise.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A loosely-typed response builder used in wide (catch-all) route handlers
|
|
6
|
+
* where the response shape is not known at compile time. Unlike the generated
|
|
7
|
+
* `GenericResponseBuilder`, this interface accepts `unknown` for all body
|
|
8
|
+
* arguments and does not enforce content-type constraints.
|
|
9
|
+
*/
|
|
10
|
+
export interface WideResponseBuilder {
|
|
11
|
+
binary: (body: Uint8Array | string) => WideResponseBuilder;
|
|
12
|
+
empty: () => WideResponseBuilder;
|
|
13
|
+
example: (name: string) => WideResponseBuilder;
|
|
14
|
+
cookie: (
|
|
15
|
+
name: string,
|
|
16
|
+
value: string,
|
|
17
|
+
options?: CookieOptions,
|
|
18
|
+
) => WideResponseBuilder;
|
|
19
|
+
header: (body: unknown) => WideResponseBuilder;
|
|
20
|
+
html: (body: unknown) => WideResponseBuilder;
|
|
21
|
+
json: (body: unknown) => WideResponseBuilder;
|
|
22
|
+
match: (contentType: string, body: unknown) => WideResponseBuilder;
|
|
23
|
+
random: () => MaybePromise<WideResponseBuilder>;
|
|
24
|
+
text: (body: unknown) => WideResponseBuilder;
|
|
25
|
+
xml: (body: unknown) => WideResponseBuilder;
|
|
26
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { pathToFileURL } from "node:url";
|
|
2
1
|
import createDebug from "debug";
|
|
3
2
|
import Koa from "koa";
|
|
4
3
|
import bodyParser from "koa-bodyparser";
|
|
5
4
|
import { koaSwagger } from "koa2-swagger-ui";
|
|
6
5
|
import { adminApiMiddleware } from "./admin-api-middleware.js";
|
|
7
6
|
import { openapiMiddleware } from "./openapi-middleware.js";
|
|
8
|
-
import { pageMiddleware } from "./page-middleware.js";
|
|
9
7
|
const debug = createDebug("counterfact:server:create-koa-app");
|
|
10
8
|
export function createKoaApp(registry, koaMiddleware, config, contextRegistry) {
|
|
11
9
|
const app = new Koa();
|
|
@@ -21,30 +19,13 @@ export function createKoaApp(registry, koaMiddleware, config, contextRegistry) {
|
|
|
21
19
|
}
|
|
22
20
|
debug("basePath: %s", config.basePath);
|
|
23
21
|
debug("routes", registry.routes);
|
|
24
|
-
app.use(pageMiddleware("/counterfact/", "index", {
|
|
25
|
-
basePath: config.basePath,
|
|
26
|
-
methods: ["get", "post", "put", "delete", "patch"],
|
|
27
|
-
openApiHref: config.openApiPath.includes("://")
|
|
28
|
-
? config.openApiPath
|
|
29
|
-
: pathToFileURL(config.openApiPath).href,
|
|
30
|
-
openApiPath: config.openApiPath,
|
|
31
|
-
get routes() {
|
|
32
|
-
return registry.routes;
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
22
|
app.use(async (ctx, next) => {
|
|
36
23
|
if (ctx.URL.pathname === "/counterfact") {
|
|
37
|
-
ctx.redirect("/counterfact/");
|
|
24
|
+
ctx.redirect("/counterfact/swagger");
|
|
38
25
|
return;
|
|
39
26
|
}
|
|
40
27
|
await next();
|
|
41
28
|
});
|
|
42
|
-
app.use(pageMiddleware("/counterfact/rapidoc", "rapi-doc", {
|
|
43
|
-
basePath: config.basePath,
|
|
44
|
-
get routes() {
|
|
45
|
-
return registry.routes;
|
|
46
|
-
},
|
|
47
|
-
}));
|
|
48
29
|
app.use(bodyParser());
|
|
49
30
|
app.use(async (ctx, next) => {
|
|
50
31
|
await next();
|
|
@@ -3,6 +3,7 @@ import createDebugger from "debug";
|
|
|
3
3
|
import fetch, { Headers } from "node-fetch";
|
|
4
4
|
import { createResponseBuilder } from "./response-builder.js";
|
|
5
5
|
import { validateRequest } from "./request-validator.js";
|
|
6
|
+
import { validateResponse } from "./response-validator.js";
|
|
6
7
|
import { Tools } from "./tools.js";
|
|
7
8
|
const debug = createDebugger("counterfact:server:dispatcher");
|
|
8
9
|
function parseCookies(cookieHeader) {
|
|
@@ -131,7 +132,7 @@ export class Dispatcher {
|
|
|
131
132
|
}
|
|
132
133
|
return false;
|
|
133
134
|
}
|
|
134
|
-
async request({ auth, body, headers = {}, method, path, query, req, }) {
|
|
135
|
+
async request({ auth, body, headers = {}, method, path, query, rawBody, req, }) {
|
|
135
136
|
debug(`request: ${method} ${path}`);
|
|
136
137
|
debug(`headers: ${JSON.stringify(headers)}`);
|
|
137
138
|
debug(`body: ${JSON.stringify(body)}`);
|
|
@@ -177,12 +178,9 @@ export class Dispatcher {
|
|
|
177
178
|
cookie: parseCookies(headers.cookie ?? headers.Cookie ?? ""),
|
|
178
179
|
headers,
|
|
179
180
|
proxy: async (url) => {
|
|
180
|
-
if (body !== undefined && headers.contentType !== "application/json") {
|
|
181
|
-
throw new Error(`$.proxy() is currently limited to application/json requests. You tried to proxy to ${url} with a Content-Type of ${headers.contentType ?? "[unknown]"}. Please open an issue at https://github.com/pmcelhaney/counterfact/issues and prod me to fix this limitation.`);
|
|
182
|
-
}
|
|
183
181
|
delete headers.host;
|
|
184
182
|
const fetchResponse = await this.fetch(`${url}${req.path ?? ""}`, {
|
|
185
|
-
body: body === undefined ? undefined :
|
|
183
|
+
body: body === undefined ? undefined : rawBody,
|
|
186
184
|
headers: new Headers(headers),
|
|
187
185
|
method,
|
|
188
186
|
});
|
|
@@ -213,6 +211,21 @@ export class Dispatcher {
|
|
|
213
211
|
status: 406,
|
|
214
212
|
};
|
|
215
213
|
}
|
|
214
|
+
if (this.config?.validateResponses !== false) {
|
|
215
|
+
const validation = validateResponse(operation, normalizedResponse);
|
|
216
|
+
if (!validation.valid) {
|
|
217
|
+
return {
|
|
218
|
+
...normalizedResponse,
|
|
219
|
+
appendedHeaders: [
|
|
220
|
+
...(normalizedResponse.appendedHeaders ?? []),
|
|
221
|
+
...validation.errors.map((error) => [
|
|
222
|
+
"response-type-error",
|
|
223
|
+
error,
|
|
224
|
+
]),
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
216
229
|
return normalizedResponse;
|
|
217
230
|
}
|
|
218
231
|
}
|