@scalar/mock-server 0.12.13 → 0.13.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/CHANGELOG.md +46 -0
- package/dist/create-asyncapi-mock-server.d.ts +8 -2
- package/dist/create-asyncapi-mock-server.d.ts.map +1 -1
- package/dist/create-asyncapi-mock-server.js +5 -2
- package/dist/create-mock-server.d.ts.map +1 -1
- package/dist/create-mock-server.js +112 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/routes/mock-any-response.d.ts +1 -4
- package/dist/routes/mock-any-response.d.ts.map +1 -1
- package/dist/routes/mock-any-response.js +43 -22
- package/dist/routes/mock-handler-response.d.ts.map +1 -1
- package/dist/routes/mock-handler-response.js +2 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/build-handler-context.d.ts.map +1 -1
- package/dist/utils/build-handler-context.js +3 -2
- package/dist/utils/collect-sse-events.d.ts +30 -0
- package/dist/utils/collect-sse-events.d.ts.map +1 -0
- package/dist/utils/collect-sse-events.js +137 -0
- package/dist/utils/hono-route-from-path.d.ts +16 -3
- package/dist/utils/hono-route-from-path.d.ts.map +1 -1
- package/dist/utils/hono-route-from-path.js +98 -5
- package/dist/utils/log-authentication-instructions.d.ts +7 -2
- package/dist/utils/log-authentication-instructions.d.ts.map +1 -1
- package/dist/utils/log-authentication-instructions.js +64 -60
- package/dist/utils/path-parameters.d.ts +13 -0
- package/dist/utils/path-parameters.d.ts.map +1 -0
- package/dist/utils/path-parameters.js +17 -0
- package/dist/utils/replace-circular-markers.d.ts +22 -0
- package/dist/utils/replace-circular-markers.d.ts.map +1 -0
- package/dist/utils/replace-circular-markers.js +226 -0
- package/dist/utils/request-matches-pinned-query.d.ts +10 -0
- package/dist/utils/request-matches-pinned-query.d.ts.map +1 -0
- package/dist/utils/request-matches-pinned-query.js +13 -0
- package/dist/utils/resolve-logger.d.ts +12 -0
- package/dist/utils/resolve-logger.d.ts.map +1 -0
- package/dist/utils/resolve-logger.js +16 -0
- package/dist/utils/serialize-response-body.d.ts +11 -0
- package/dist/utils/serialize-response-body.d.ts.map +1 -0
- package/dist/utils/serialize-response-body.js +88 -0
- package/dist/utils/split-path-key.d.ts +30 -0
- package/dist/utils/split-path-key.d.ts.map +1 -0
- package/dist/utils/split-path-key.js +77 -0
- package/dist/utils/validate-request.d.ts.map +1 -1
- package/dist/utils/validate-request.js +13 -2
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @scalar/mock-server
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10051](https://github.com/scalar/scalar/pull/10051): feat: add a `logger` option to control startup logging
|
|
8
|
+
|
|
9
|
+
`createMockServer()` prints authentication instructions for the security schemes of the document when it starts. That is helpful in a terminal, but it is noise when the mock server runs inside a test harness or another program, which so far left callers replacing the global console.
|
|
10
|
+
|
|
11
|
+
Pass `logger: false` to silence those instructions, or a `(line: string) => void` sink to route them elsewhere:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const app = await createMockServer({ document, logger: false })
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Diagnostics are not affected: warnings and errors about security schemes the mock server cannot handle, request validator compilation errors, and `x-seed` errors are printed either way.
|
|
18
|
+
|
|
19
|
+
`createAsyncApiMockServer()` already accepted a `logger` sink; it now takes the same `boolean | ((line: string) => void)` shape, so both factories are silenced and redirected the same way. It stays silent by default — pass `logger: true` to print its transport lifecycle lines.
|
|
20
|
+
|
|
21
|
+
The `MockServerLogger` type is now exported as well, so a custom sink can be typed outside of the package.
|
|
22
|
+
|
|
23
|
+
- [#10037](https://github.com/scalar/scalar/pull/10037): Frame `text/event-stream` responses as Server-Sent Events instead of returning a single JSON body with an SSE content type. Each event is written as a `data:` line terminated by a blank line, then the stream closes.
|
|
24
|
+
- Named `examples` are read as the sequence of events the endpoint emits, in declaration order (`Prefer: example=<name>` still pins the stream to that one example).
|
|
25
|
+
- An array example is read as the event sequence too, one event per item.
|
|
26
|
+
- An example that already spells out SSE framing (`data:` and `event:` lines, or a `:` comment heartbeat) is written as its own framing, with only its terminating blank line normalized, instead of being wrapped in a second `data:` line. Such examples describe a whole stream, so a map of them is read as alternatives and the first one is served.
|
|
27
|
+
- With no example, the schema-generated payload is emitted three times so the stream has more than one event to iterate — unless the schema already generates a sequence (a multi-item array, or a string that spells the wire format out), which is sent once, not repeated.
|
|
28
|
+
|
|
29
|
+
- [#10038](https://github.com/scalar/scalar/pull/10038): Answer unhandled errors with a structured JSON `500` naming the operation that failed, instead of the previous plain-text `Internal Server Error`. The body reports the error `message` along with the matched operation's `method`, OpenAPI `path`, and `operationId` (when the document declares one), and the error is still logged to the console. Errors that already carry their own response keep the status and body they chose.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#9967](https://github.com/scalar/scalar/pull/9967): Align the `ajv` and `ajv-formats` dependencies with the shared workspace catalog (`ajv@^8.20.0`).
|
|
34
|
+
- [#10039](https://github.com/scalar/scalar/pull/10039): Fix request validation falling open for recursive schemas. Resolving a schema that references itself leaves a `'[circular]'` marker where the cycle was cut, which Ajv refused to compile, so the mock server logged an error and skipped validating the request body — or every parameter in that location. The recursion point now compiles as an always-valid schema, and a constraint that would flip that into a stricter one (`not`, `if`, `oneOf`, `contains`, and the keywords that only qualify them) is dropped, so the rest of the schema is enforced again without rejecting requests the document allows.
|
|
35
|
+
- [#10035](https://github.com/scalar/scalar/pull/10035): Fix JSON responses for primitive string bodies. A response declaring `application/json` with a `type: string` schema (or a plain string example) was written to the wire verbatim, so clients received the bare characters `string` instead of `"string"` and could not parse the payload.
|
|
36
|
+
|
|
37
|
+
A string body is now JSON-encoded whenever the negotiated media type carries a single JSON document, which includes suffixed types such as `application/problem+json` and parameterized ones such as `application/json; charset=utf-8`. Two things keep their raw string: every other media type, where the characters are already the payload (`text/plain`, `text/event-stream`, XML, and the line-delimited JSON types), and a body that is already the value the schema describes — anything that parses when the schema declares a non-string type, or a JSON object or array when the schema says nothing, both of which are documents the author serialized by hand.
|
|
38
|
+
|
|
39
|
+
XML is now matched on the parsed media type subtype rather than by substring, so only a genuine XML type is serialized as an XML document. A media type that merely contains `xml` somewhere, such as one carrying it in a parameter, no longer is.
|
|
40
|
+
|
|
41
|
+
- [#10044](https://github.com/scalar/scalar/pull/10044): fix: escape spec-derived path keys and route path keys that carry a query string
|
|
42
|
+
|
|
43
|
+
Path keys such as `/v1/messages?beta=true` used to be registered verbatim as routes, where the query string was read as routing syntax. On a parameterized path that made the router compile an invalid regular expression and every single request failed with an empty `500`. The query is now peeled off and matched against the incoming request, so `/v1/messages?beta=true` answers only requests that send `beta=true` and `/v1/messages` keeps answering the rest.
|
|
44
|
+
|
|
45
|
+
Characters that would otherwise be read as routing syntax (`:`, `*`, `|`, and braces outside a path parameter) are escaped, so a path key can no longer act as a pattern. Note that this also applies to `*`: a path key ending in `*` is now served as a literal path instead of matching everything below it.
|
|
46
|
+
|
|
47
|
+
One limitation is worth knowing: Hono allows a single parameter per path segment, so a segment that mixes a path parameter with escaped literal text (`/v1/jobs/{jobId}:cancel`) routes to the right operation but does not bind `jobId` by name. Request validation reads it as missing, so a document describing such a path needs the server-wide `validateRequest: false` for now.
|
|
48
|
+
|
|
3
49
|
## 0.12.13
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createNodeWebSocket } from '@hono/node-ws';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import type { MessageDirection, MockTransport } from './transports/types.js';
|
|
4
|
+
import type { MockServerLogger } from './types.js';
|
|
4
5
|
/** Options for {@link createAsyncApiMockServer}. */
|
|
5
6
|
export type AsyncApiMockServerOptions = {
|
|
6
7
|
/**
|
|
@@ -20,8 +21,13 @@ export type AsyncApiMockServerOptions = {
|
|
|
20
21
|
direction: MessageDirection;
|
|
21
22
|
payload: unknown;
|
|
22
23
|
}) => void;
|
|
23
|
-
/**
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Control the transport lifecycle log lines the server prints. Pass `true` to log them to the
|
|
26
|
+
* console, a `(line) => void` sink to route them elsewhere, or `false` (the default) to stay silent.
|
|
27
|
+
*
|
|
28
|
+
* Diagnostics such as a channel with no matching transport are printed either way.
|
|
29
|
+
*/
|
|
30
|
+
logger?: boolean | MockServerLogger;
|
|
25
31
|
};
|
|
26
32
|
/** The result of {@link createAsyncApiMockServer}. */
|
|
27
33
|
export type AsyncApiMockServer = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAE5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sEAAsE;IACtE,GAAG,EAAE,IAAI,CAAA;IACT;;;OAGG;IACH,eAAe,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,iBAAiB,CAAC,CAAA;CAC3E,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0C9G"}
|
|
@@ -5,6 +5,7 @@ import { defaultTransports } from './transports/index.js';
|
|
|
5
5
|
import { generateMessage } from './utils/generate-message.js';
|
|
6
6
|
import { processAsyncApiDocument } from './utils/process-asyncapi-document.js';
|
|
7
7
|
import { resolveChannels } from './utils/resolve-channels.js';
|
|
8
|
+
import { resolveLogger } from './utils/resolve-logger.js';
|
|
8
9
|
/**
|
|
9
10
|
* Create a mock server for an AsyncAPI 3.1 document — the event-driven counterpart of
|
|
10
11
|
* {@link createMockServer}. Each channel is registered on a transport (WebSocket or SSE by
|
|
@@ -28,7 +29,7 @@ export async function createAsyncApiMockServer(options) {
|
|
|
28
29
|
const document = await processAsyncApiDocument(options.document);
|
|
29
30
|
const channels = resolveChannels(document);
|
|
30
31
|
const transports = [...defaultTransports, ...(options.transports ?? [])];
|
|
31
|
-
const log = options.logger
|
|
32
|
+
const log = resolveLogger(options.logger, false);
|
|
32
33
|
// CORS for the SSE/HTTP routes (WebSocket upgrades are not subject to CORS).
|
|
33
34
|
app.use(cors());
|
|
34
35
|
const context = {
|
|
@@ -41,7 +42,9 @@ export async function createAsyncApiMockServer(options) {
|
|
|
41
42
|
for (const channel of channels) {
|
|
42
43
|
const transport = transports.find((candidate) => candidate.supports(channel));
|
|
43
44
|
if (!transport) {
|
|
44
|
-
|
|
45
|
+
// A channel with no matching transport is a configuration problem, so surface it through the
|
|
46
|
+
// console unconditionally rather than the (silenceable) logger.
|
|
47
|
+
console.warn(`[asyncapi] no transport for channel "${channel.id}" (protocols: ${channel.protocols.join(', ') || 'none'})`);
|
|
45
48
|
continue;
|
|
46
49
|
}
|
|
47
50
|
transport.register(channel, context);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,IAAI,EAA0B,MAAM,MAAM,CAAA;AAIjE,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAwD5D;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8NtF"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
+
import { every } from 'hono/combine';
|
|
3
4
|
import { cors } from 'hono/cors';
|
|
4
5
|
import { buildSeedContext } from './utils/build-seed-context.js';
|
|
5
6
|
import { executeSeed } from './utils/execute-seed.js';
|
|
@@ -9,17 +10,62 @@ import { honoRouteFromPath } from './utils/hono-route-from-path.js';
|
|
|
9
10
|
import { isAuthenticationRequired } from './utils/is-authentication-required.js';
|
|
10
11
|
import { logAuthenticationInstructions } from './utils/log-authentication-instructions.js';
|
|
11
12
|
import { processOpenApiDocument } from './utils/process-openapi-document.js';
|
|
13
|
+
import { requestMatchesPinnedQuery } from './utils/request-matches-pinned-query.js';
|
|
14
|
+
import { resolveLogger } from './utils/resolve-logger.js';
|
|
12
15
|
import { setUpAuthenticationRoutes } from './utils/set-up-authentication-routes.js';
|
|
16
|
+
import { splitPathKey } from './utils/split-path-key.js';
|
|
13
17
|
import { validateRequest } from './utils/validate-request.js';
|
|
14
18
|
import { store } from './libs/store.js';
|
|
15
19
|
import { mockAnyResponse } from './routes/mock-any-response.js';
|
|
16
20
|
import { mockHandlerResponse } from './routes/mock-handler-response.js';
|
|
17
21
|
import { respondWithOpenApiDocument } from './routes/respond-with-openapi-document.js';
|
|
22
|
+
/** Record which operation a request matched, so an error escaping it can be named */
|
|
23
|
+
const setMockedOperation = (c, operation) => {
|
|
24
|
+
;
|
|
25
|
+
c.set('mockedOperation', operation);
|
|
26
|
+
};
|
|
27
|
+
/** Read back the operation a request matched, or `undefined` when it reached no mocked route */
|
|
28
|
+
const getMockedOperation = (c) => c.get('mockedOperation');
|
|
29
|
+
/**
|
|
30
|
+
* Whether an error carries its own response, the way Hono's `HTTPException` does.
|
|
31
|
+
*
|
|
32
|
+
* Checked structurally rather than with `instanceof`, exactly like Hono's own default handler, so an
|
|
33
|
+
* `HTTPException` thrown from a second copy of Hono is still recognized.
|
|
34
|
+
*/
|
|
35
|
+
const carriesResponse = (error) => 'getResponse' in error && typeof error.getResponse === 'function';
|
|
18
36
|
/**
|
|
19
37
|
* Create a mock server instance
|
|
20
38
|
*/
|
|
21
39
|
export async function createMockServer(configuration) {
|
|
22
40
|
const app = new Hono();
|
|
41
|
+
// Unhandled errors would otherwise reach Hono's default handler, which answers with a plain-text
|
|
42
|
+
// `Internal Server Error` that says nothing about what broke. A mock server fails for mundane,
|
|
43
|
+
// document-shaped reasons — a response header name the runtime rejects, an example that cannot be
|
|
44
|
+
// serialized — so answer with JSON that names the failing operation and repeats the message,
|
|
45
|
+
// which makes the failure readable in the response instead of something to go reproduce.
|
|
46
|
+
app.onError((error, c) => {
|
|
47
|
+
// The status and body such an error chose are deliberate, not an internal failure, so answer
|
|
48
|
+
// with them. Unlike Hono's default handler, which returns the response directly, it is rebuilt
|
|
49
|
+
// through the context so headers already staged on the response — those from the CORS
|
|
50
|
+
// middleware, most of all — still apply.
|
|
51
|
+
if (carriesResponse(error)) {
|
|
52
|
+
const response = error.getResponse();
|
|
53
|
+
return c.newResponse(response.body, response);
|
|
54
|
+
}
|
|
55
|
+
const operation = getMockedOperation(c);
|
|
56
|
+
// Keep logging the error, as Hono's default handler does, so the stack trace is not lost. Routes
|
|
57
|
+
// added to the returned app are not mocked operations, so name the concrete request instead —
|
|
58
|
+
// the log always points somewhere.
|
|
59
|
+
console.error(operation
|
|
60
|
+
? `Error while mocking ${operation.method} ${operation.path}:`
|
|
61
|
+
: `Error handling ${c.req.method} ${c.req.path}:`, error);
|
|
62
|
+
return c.json({
|
|
63
|
+
error: 'Internal Server Error',
|
|
64
|
+
message: error.message,
|
|
65
|
+
// Left out entirely rather than sent as `null`, so a client can test for the key.
|
|
66
|
+
...(operation ? { operation } : {}),
|
|
67
|
+
}, 500);
|
|
68
|
+
});
|
|
23
69
|
/** Dereferenced OpenAPI document */
|
|
24
70
|
const schema = await processOpenApiDocument(configuration?.document ?? configuration?.specification);
|
|
25
71
|
// Seed data from schemas with x-seed extension
|
|
@@ -52,10 +98,34 @@ export async function createMockServer(configuration) {
|
|
|
52
98
|
app.use(cors());
|
|
53
99
|
/** Authentication methods defined in the OpenAPI document */
|
|
54
100
|
setUpAuthenticationRoutes(app, schema);
|
|
55
|
-
|
|
101
|
+
// Only the instructions honor `logger` (on by default); the util still prints warnings and errors
|
|
102
|
+
// about security schemes the mock server cannot handle, which a silenced startup should surface.
|
|
103
|
+
logAuthenticationInstructions(schema?.components?.securitySchemes || {}, resolveLogger(configuration.logger, true));
|
|
56
104
|
/** Paths specified in the OpenAPI document */
|
|
57
105
|
const paths = schema?.paths ?? {};
|
|
58
|
-
|
|
106
|
+
// A path key may pin query parameters to describe a variant of an operation, for example
|
|
107
|
+
// `/v1/messages?beta=true` next to `/v1/messages`. Hono runs every matching route in registration
|
|
108
|
+
// order, so a variant has to come before the sibling it shares a path with — otherwise the sibling
|
|
109
|
+
// answers its requests too, and the more pinned parameters a key has the more specific it is.
|
|
110
|
+
const pathKeys = Object.keys(paths).map((path) => {
|
|
111
|
+
const { path: pathname, query } = splitPathKey(path);
|
|
112
|
+
return { path, pathname, query };
|
|
113
|
+
});
|
|
114
|
+
/** Where each path first shows up in the document, so its variants stay with it */
|
|
115
|
+
const documentOrder = new Map();
|
|
116
|
+
pathKeys.forEach(({ pathname }, index) => {
|
|
117
|
+
if (!documentOrder.has(pathname)) {
|
|
118
|
+
documentOrder.set(pathname, index);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Keys that share a path are grouped where the first of them appears and the most specific one
|
|
122
|
+
// leads the group; a key with a path of its own never moves. So the only keys that change places
|
|
123
|
+
// with anything unrelated are the variants of a path that is described more than once. When two
|
|
124
|
+
// distinct paths overlap — a literal and a parameterized one that pins a query — neither is moved,
|
|
125
|
+
// so whichever the document declares first is matched first, the same as any pair of overlapping
|
|
126
|
+
// Hono routes.
|
|
127
|
+
const orderedPathKeys = [...pathKeys].sort((a, b) => (documentOrder.get(a.pathname) ?? 0) - (documentOrder.get(b.pathname) ?? 0) || b.query.length - a.query.length);
|
|
128
|
+
orderedPathKeys.forEach(({ path, query }) => {
|
|
59
129
|
// A path item may itself be a `$ref`, so resolve it before reading its operations.
|
|
60
130
|
const pathItem = getResolvedRef(paths[path]);
|
|
61
131
|
const methods = Object.keys(getOperations(pathItem));
|
|
@@ -63,17 +133,37 @@ export async function createMockServer(configuration) {
|
|
|
63
133
|
methods.forEach((method) => {
|
|
64
134
|
const route = honoRouteFromPath(path);
|
|
65
135
|
const operation = pathItem?.[method];
|
|
136
|
+
// Remember which operation this route mocks, so the error handler can name it when something
|
|
137
|
+
// fails downstream. Recorded on the context rather than mapped back from the request path,
|
|
138
|
+
// which would not survive the app being mounted under a base path. Registered before the rest
|
|
139
|
+
// of the route so a failure in request validation is named too. The OpenAPI path key is kept
|
|
140
|
+
// (rather than the Hono route) because that is what the document author reads.
|
|
141
|
+
const mockedOperation = {
|
|
142
|
+
// `toUpperCase` widens to `string`, so restate the narrower type the method union guarantees.
|
|
143
|
+
method: method.toUpperCase(),
|
|
144
|
+
path,
|
|
145
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {}),
|
|
146
|
+
};
|
|
147
|
+
/** Middleware chain answering this operation, in the order it runs */
|
|
148
|
+
const handlers = [];
|
|
149
|
+
// Runs first, so the error handler can name the operation even when validation fails. For a
|
|
150
|
+
// path key that pins a query it is part of the guarded chain below, so it only fires once the
|
|
151
|
+
// request actually matches the variant rather than for one that is handed on to the sibling.
|
|
152
|
+
handlers.push(async (c, next) => {
|
|
153
|
+
setMockedOperation(c, mockedOperation);
|
|
154
|
+
await next();
|
|
155
|
+
});
|
|
66
156
|
// Operation-level security overrides the global requirement, so fall back to the
|
|
67
157
|
// document-wide `security` when the operation does not define its own.
|
|
68
158
|
const effectiveSecurity = operation.security ?? schema?.security;
|
|
69
159
|
// Check if authentication is required for this operation
|
|
70
160
|
if (isAuthenticationRequired(effectiveSecurity)) {
|
|
71
|
-
|
|
161
|
+
handlers.push(handleAuthentication(schema, operation));
|
|
72
162
|
}
|
|
73
163
|
// Notify the `onRequest` callback before validation runs, so it fires for every request —
|
|
74
164
|
// including ones the validation middleware rejects with a `422`.
|
|
75
165
|
if (configuration.onRequest) {
|
|
76
|
-
|
|
166
|
+
handlers.push(async (c, next) => {
|
|
77
167
|
configuration.onRequest?.({ context: c, operation });
|
|
78
168
|
await next();
|
|
79
169
|
});
|
|
@@ -82,7 +172,7 @@ export async function createMockServer(configuration) {
|
|
|
82
172
|
// opt out with `validateRequest: false`). Runs after authentication but before the
|
|
83
173
|
// mock handler. Validators are compiled once here, so there is no per-request recompilation.
|
|
84
174
|
if (configuration.validateRequest !== false) {
|
|
85
|
-
|
|
175
|
+
handlers.push(validateRequest(operation, pathItem?.parameters));
|
|
86
176
|
}
|
|
87
177
|
// Check if operation has x-handler extension
|
|
88
178
|
// Validate that it's a non-empty string (consistent with x-seed validation)
|
|
@@ -90,11 +180,26 @@ export async function createMockServer(configuration) {
|
|
|
90
180
|
const hasHandler = handlerCode && typeof handlerCode === 'string' && handlerCode.trim().length > 0;
|
|
91
181
|
// Route to appropriate handler
|
|
92
182
|
if (hasHandler) {
|
|
93
|
-
|
|
183
|
+
handlers.push(async (c) => await mockHandlerResponse(c, operation));
|
|
94
184
|
}
|
|
95
185
|
else {
|
|
96
|
-
|
|
186
|
+
handlers.push(async (c) => await mockAnyResponse(c, operation));
|
|
187
|
+
}
|
|
188
|
+
if (query.length === 0) {
|
|
189
|
+
handlers.forEach((handler) => app[method](route, handler));
|
|
190
|
+
return;
|
|
97
191
|
}
|
|
192
|
+
// The pinned query parameters are not part of the route, so they are checked here. A request
|
|
193
|
+
// that does not carry them is handed on to the next matching route — usually the sibling path
|
|
194
|
+
// key without the query string.
|
|
195
|
+
const operationChain = every(...handlers);
|
|
196
|
+
app[method](route, async (c, next) => {
|
|
197
|
+
if (!requestMatchesPinnedQuery(c, query)) {
|
|
198
|
+
await next();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await operationChain(c, next);
|
|
202
|
+
});
|
|
98
203
|
});
|
|
99
204
|
});
|
|
100
205
|
// OpenAPI JSON file
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { type AsyncApiMockServer, type AsyncApiMockServerOptions, createAsyncApi
|
|
|
2
2
|
export { createMockServer } from './create-mock-server.js';
|
|
3
3
|
export { defaultTransports, sseTransport, websocketTransport } from './transports/index.js';
|
|
4
4
|
export type { MessageDirection, MockMessage, MockTransport, ResolvedChannel, ResolvedMessage, ResolvedOperation, TransportContext, } from './transports/types.js';
|
|
5
|
+
export type { MockServerLogger, MockServerOptions } from './types.js';
|
|
5
6
|
export { isAsyncApiDocument } from './utils/process-asyncapi-document.js';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,wBAAwB,GACzB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAClF,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,wBAAwB,GACzB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAClF,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA"}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
2
2
|
import type { Context } from 'hono';
|
|
3
|
-
import type { StatusCode } from 'hono/utils/http-status';
|
|
4
3
|
/**
|
|
5
4
|
* Mock any response
|
|
6
5
|
*/
|
|
7
|
-
export declare function mockAnyResponse(c: Context, operation: OpenAPIV3_1.OperationObject):
|
|
8
|
-
error: string;
|
|
9
|
-
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<null, StatusCode, "body">) | (Response & import("hono").TypedResponse<string, import("hono/utils/http-status").ContentfulStatusCode, "body">);
|
|
6
|
+
export declare function mockAnyResponse(c: Context, operation: OpenAPIV3_1.OperationObject): Response;
|
|
10
7
|
//# sourceMappingURL=mock-any-response.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAanC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,YA6HjF"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { json2xml } from '@scalar/helpers/file/json2xml';
|
|
2
1
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
2
|
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
4
3
|
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
5
4
|
import { accepts } from 'hono/accepts';
|
|
5
|
+
import { streamSSE } from 'hono/streaming';
|
|
6
|
+
import { collectSseEvents, isEventStreamContentType } from '../utils/collect-sse-events.js';
|
|
6
7
|
import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
|
|
7
8
|
import { normalizeResponseBody } from '../utils/normalize-response-body.js';
|
|
8
9
|
import { parsePreferHeader } from '../utils/parse-prefer-header.js';
|
|
10
|
+
import { pathParameters } from '../utils/path-parameters.js';
|
|
9
11
|
import { selectResponseExample } from '../utils/select-response-example.js';
|
|
12
|
+
import { serializeResponseBody } from '../utils/serialize-response-body.js';
|
|
10
13
|
/**
|
|
11
14
|
* Mock any response
|
|
12
15
|
*/
|
|
@@ -61,33 +64,51 @@ export function mockAnyResponse(c, operation) {
|
|
|
61
64
|
});
|
|
62
65
|
c.header('Content-Type', acceptedContentType);
|
|
63
66
|
const acceptedResponse = selectedResponse?.content?.[acceptedContentType];
|
|
67
|
+
const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
|
|
68
|
+
/** Generates the response body from the schema, or returns `undefined` when there is no schema. */
|
|
69
|
+
const generateFromSchema = () => responseSchema
|
|
70
|
+
? getExampleFromSchema(responseSchema, {
|
|
71
|
+
emptyString: 'string',
|
|
72
|
+
variables: pathParameters(c),
|
|
73
|
+
mode: 'read',
|
|
74
|
+
})
|
|
75
|
+
: undefined;
|
|
76
|
+
// Server-Sent Events are a framed, multi-event wire format, so they cannot go out as one buffered
|
|
77
|
+
// body: a client reading the stream expects `data:` lines terminated by a blank line. Everything
|
|
78
|
+
// else (JSON, XML, text) keeps taking the single-body path below.
|
|
79
|
+
if (isEventStreamContentType(acceptedContentType)) {
|
|
80
|
+
const events = collectSseEvents(acceptedResponse, {
|
|
81
|
+
exampleName: prefer.example,
|
|
82
|
+
generate: generateFromSchema,
|
|
83
|
+
});
|
|
84
|
+
c.status(statusCode);
|
|
85
|
+
// `streamSSE` sets the transport headers itself (`Content-Type`, `Cache-Control`, `Connection`,
|
|
86
|
+
// `Transfer-Encoding`), so those win over a value the document declared for the same header —
|
|
87
|
+
// they are what makes the stream readable. Every other declared header set above survives.
|
|
88
|
+
return streamSSE(c, async (stream) => {
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (event.framed) {
|
|
91
|
+
await stream.write(event.text);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await stream.writeSSE({ data: event.text });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
64
99
|
// Body: a named/singular/first example if one is defined, otherwise generate
|
|
65
100
|
// a value from the schema. `Prefer: example=<name>` picks a named example.
|
|
66
101
|
const selectedExample = selectResponseExample(acceptedResponse, prefer.example);
|
|
67
|
-
const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
|
|
68
102
|
const body = selectedExample
|
|
69
103
|
? normalizeResponseBody(selectedExample.value, responseSchema)
|
|
70
104
|
: responseSchema
|
|
71
|
-
? normalizeResponseBody(
|
|
72
|
-
emptyString: 'string',
|
|
73
|
-
variables: c.req.param(),
|
|
74
|
-
mode: 'read',
|
|
75
|
-
}), responseSchema)
|
|
105
|
+
? normalizeResponseBody(generateFromSchema(), responseSchema)
|
|
76
106
|
: null;
|
|
77
107
|
c.status(statusCode);
|
|
78
|
-
|
|
79
|
-
// `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
acceptedContentType?.includes('xml')
|
|
85
|
-
? json2xml(body)
|
|
86
|
-
: // JSON
|
|
87
|
-
JSON.stringify(body)
|
|
88
|
-
: typeof body === 'string'
|
|
89
|
-
? // String
|
|
90
|
-
body
|
|
91
|
-
: // null / number / boolean
|
|
92
|
-
JSON.stringify(body));
|
|
108
|
+
const serializedBody = serializeResponseBody(body, acceptedContentType, responseSchema);
|
|
109
|
+
// `JSON.stringify` returns `undefined` for an `undefined` body, which is an empty response.
|
|
110
|
+
if (serializedBody === undefined) {
|
|
111
|
+
return c.body(null);
|
|
112
|
+
}
|
|
113
|
+
return c.body(serializedBody);
|
|
93
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAqIxD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,gMA8D3F"}
|
|
@@ -5,6 +5,7 @@ import { buildHandlerContext } from '../utils/build-handler-context.js';
|
|
|
5
5
|
import { executeHandler } from '../utils/execute-handler.js';
|
|
6
6
|
import { normalizeResponseBody } from '../utils/normalize-response-body.js';
|
|
7
7
|
import { parsePreferHeader } from '../utils/parse-prefer-header.js';
|
|
8
|
+
import { pathParameters } from '../utils/path-parameters.js';
|
|
8
9
|
import { selectResponseExample } from '../utils/select-response-example.js';
|
|
9
10
|
/**
|
|
10
11
|
* Get example response from OpenAPI spec for a given status code.
|
|
@@ -48,7 +49,7 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
|
|
|
48
49
|
: responseSchema
|
|
49
50
|
? normalizeResponseBody(getExampleFromSchema(responseSchema, {
|
|
50
51
|
emptyString: 'string',
|
|
51
|
-
variables: c
|
|
52
|
+
variables: pathParameters(c),
|
|
52
53
|
mode: 'read',
|
|
53
54
|
}), responseSchema)
|
|
54
55
|
: null;
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type HttpMethod = (typeof httpMethods)[number];
|
|
|
10
10
|
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> & {
|
|
11
11
|
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
|
|
12
12
|
}[Keys];
|
|
13
|
+
/** A sink for the informational log lines the mock servers print while starting up. */
|
|
14
|
+
export type MockServerLogger = (line: string) => void;
|
|
13
15
|
type BaseMockServerOptions = {
|
|
14
16
|
/**
|
|
15
17
|
* The OpenAPI document to use for mocking.
|
|
@@ -43,6 +45,19 @@ type BaseMockServerOptions = {
|
|
|
43
45
|
* always return a mock response regardless of whether the request matches the contract.
|
|
44
46
|
*/
|
|
45
47
|
validateRequest?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Control the informational output the server prints while starting up, which is currently the
|
|
50
|
+
* authentication instructions for the security schemes of the document. Defaults to logging to
|
|
51
|
+
* the console.
|
|
52
|
+
*
|
|
53
|
+
* Pass `false` to silence it — handy when the mock server runs inside a test harness or another
|
|
54
|
+
* program, where the instructions are noise — or a `(line) => void` sink to route the lines
|
|
55
|
+
* somewhere other than the console.
|
|
56
|
+
*
|
|
57
|
+
* Diagnostics are not affected: warnings and errors about security schemes the mock server cannot
|
|
58
|
+
* handle, request validator compilation errors, and `x-seed` errors are printed either way.
|
|
59
|
+
*/
|
|
60
|
+
logger?: boolean | MockServerLogger;
|
|
46
61
|
};
|
|
47
62
|
export type MockServerOptions = RequireAtLeastOne<BaseMockServerOptions, 'specification' | 'document'>;
|
|
48
63
|
export {};
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,6CAA6C;AAC7C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AAExF,wBAAwB;AACxB,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAErD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,GACzF;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAA;AAET,KAAK,qBAAqB,GAAG;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC,eAAe,CAAA;KAAE,KAAK,IAAI,CAAA;IAExF;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,6CAA6C;AAC7C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AAExF,wBAAwB;AACxB,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAErD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,GACzF;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAA;AAET,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAErD,KAAK,qBAAqB,GAAG;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC,eAAe,CAAA;KAAE,KAAK,IAAI,CAAA;IAExF;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,qBAAqB,EAAE,eAAe,GAAG,UAAU,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAMnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,CAAA;IACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CAAA;AA4DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAiD/B"}
|
|
@@ -5,6 +5,7 @@ import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
|
5
5
|
import { accepts } from 'hono/accepts';
|
|
6
6
|
import { store } from '../libs/store.js';
|
|
7
7
|
import { normalizeResponseBody } from './normalize-response-body.js';
|
|
8
|
+
import { pathParameters } from './path-parameters.js';
|
|
8
9
|
import { createStoreWrapper } from './store-wrapper.js';
|
|
9
10
|
/**
|
|
10
11
|
* Get example response from OpenAPI spec for a given status code.
|
|
@@ -42,7 +43,7 @@ function getExampleFromResponse(c, statusCode, responses) {
|
|
|
42
43
|
: responseSchema
|
|
43
44
|
? normalizeResponseBody(getExampleFromSchema(responseSchema, {
|
|
44
45
|
emptyString: 'string',
|
|
45
|
-
variables: c
|
|
46
|
+
variables: pathParameters(c),
|
|
46
47
|
mode: 'read',
|
|
47
48
|
}), responseSchema)
|
|
48
49
|
: null;
|
|
@@ -86,7 +87,7 @@ export async function buildHandlerContext(c, operation) {
|
|
|
86
87
|
faker,
|
|
87
88
|
req: {
|
|
88
89
|
body,
|
|
89
|
-
params: c
|
|
90
|
+
params: pathParameters(c),
|
|
90
91
|
query: Object.fromEntries(new URL(c.req.url).searchParams.entries()),
|
|
91
92
|
headers: Object.fromEntries(Object.entries(c.req.header()).map(([key, value]) => [key, value ?? ''])),
|
|
92
93
|
},
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
2
|
+
/** One event of a mocked Server-Sent Events response. */
|
|
3
|
+
type SseEvent = {
|
|
4
|
+
/** Already-framed event text when `framed` is true, otherwise the `data` payload of one event. */
|
|
5
|
+
text: string;
|
|
6
|
+
/** Whether `text` is SSE framing of its own, to be written as is rather than wrapped in a `data:` line. */
|
|
7
|
+
framed: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The events a `text/event-stream` response emits, in order.
|
|
11
|
+
*
|
|
12
|
+
* Examples win over the schema, mirroring `selectResponseExample`, but a stream reads them as a
|
|
13
|
+
* sequence rather than a single body:
|
|
14
|
+
* 1. A named example requested via `Prefer: example=<name>`.
|
|
15
|
+
* 2. The singular `example` keyword.
|
|
16
|
+
* 3. Every entry of the `examples` map, in declaration order — an event stream that documents a
|
|
17
|
+
* `summary` and a `row` example is documenting the two events it sends. A map of nothing but
|
|
18
|
+
* complete framing is the exception: those are alternative streams, so only the first is served.
|
|
19
|
+
* 4. Nothing declared: a schema-generated payload, repeated so the stream has more than one event.
|
|
20
|
+
*
|
|
21
|
+
* `generate` is a callback so the schema is only turned into an example when no example is declared.
|
|
22
|
+
*/
|
|
23
|
+
export declare const collectSseEvents: (mediaType: OpenAPIV3_1.MediaTypeObject | undefined, { exampleName, generate }: {
|
|
24
|
+
exampleName?: string;
|
|
25
|
+
generate: () => unknown;
|
|
26
|
+
}) => SseEvent[];
|
|
27
|
+
/** Whether a media type is Server-Sent Events, ignoring parameters such as `; charset=utf-8`. */
|
|
28
|
+
export declare const isEventStreamContentType: (contentType: string | undefined) => boolean;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=collect-sse-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-sse-events.d.ts","sourceRoot":"","sources":["../../src/utils/collect-sse-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAcxD,yDAAyD;AACzD,KAAK,QAAQ,GAAG;IACd,kGAAkG;IAClG,IAAI,EAAE,MAAM,CAAA;IACZ,2GAA2G;IAC3G,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAgFD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,WAAW,CAAC,eAAe,GAAG,SAAS,EAClD,2BAA2B;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,OAAO,CAAA;CAAE,KAC3E,QAAQ,EAoDV,CAAA;AAED,iGAAiG;AACjG,eAAO,MAAM,wBAAwB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,OACf,CAAA"}
|