@scalar/mock-server 0.15.0 → 0.17.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 +30 -0
- package/dist/create-asyncapi-mock-server.d.ts +2 -0
- package/dist/create-asyncapi-mock-server.d.ts.map +1 -1
- package/dist/create-asyncapi-mock-server.js +1 -1
- package/dist/create-mock-server.d.ts.map +1 -1
- package/dist/create-mock-server.js +25 -14
- package/dist/routes/mock-any-response.d.ts +1 -1
- package/dist/routes/mock-any-response.d.ts.map +1 -1
- package/dist/routes/mock-any-response.js +20 -5
- package/dist/routes/mock-handler-response.d.ts +1 -1
- package/dist/routes/mock-handler-response.d.ts.map +1 -1
- package/dist/routes/mock-handler-response.js +34 -7
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/build-handler-context.d.ts +3 -2
- package/dist/utils/build-handler-context.d.ts.map +1 -1
- package/dist/utils/build-handler-context.js +14 -2
- package/dist/utils/get-operation.d.ts +4 -2
- package/dist/utils/get-operation.d.ts.map +1 -1
- package/dist/utils/get-operation.js +9 -5
- package/dist/utils/process-asyncapi-document.d.ts +2 -1
- package/dist/utils/process-asyncapi-document.d.ts.map +1 -1
- package/dist/utils/process-asyncapi-document.js +10 -2
- package/dist/utils/querystring-parameter.d.ts +11 -0
- package/dist/utils/querystring-parameter.d.ts.map +1 -0
- package/dist/utils/querystring-parameter.js +135 -0
- package/dist/utils/select-response-example.d.ts +7 -3
- package/dist/utils/select-response-example.d.ts.map +1 -1
- package/dist/utils/select-response-example.js +16 -6
- package/dist/utils/serialize-response-body.d.ts +1 -1
- package/dist/utils/serialize-response-body.d.ts.map +1 -1
- package/dist/utils/serialize-response-body.js +15 -11
- package/dist/utils/validate-request.d.ts.map +1 -1
- package/dist/utils/validate-request.js +29 -1
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @scalar/mock-server
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10192](https://github.com/scalar/scalar/pull/10192): Mock-server XML response bytes now use the shared schema-aware serializer instead of `json2xml`, including attributes, namespaces, and root naming. Existing XML response snapshots may need updating. Supplied serialized XML remains unchanged.
|
|
8
|
+
|
|
9
|
+
Generate XML examples from schema metadata, preserving attributes, namespaces, array wrappers, repeated elements, and OpenAPI 3.2 text and CDATA nodes. Use the same XML serialization for request bodies, code snippets, response examples, mock responses, and Markdown documentation. Preserve serialized media examples and escape schema string examples as element text.
|
|
10
|
+
|
|
11
|
+
Explain XML generation failures in response example panels, including the serialized-example escape hatch for large payloads. Expose XML generation failures in mock response headers with `X-Scalar-XML-Error`, containing the first error diagnostic code. Report diagnostics to other consumers through a callback or the developer console, and format element-only descendants within mixed content without changing text values.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#10343](https://github.com/scalar/scalar/pull/10343): Restrict AsyncAPI external references to the source directory and public network addresses, and preserve the source location for preloaded documents.
|
|
16
|
+
|
|
17
|
+
URL inputs to `createAsyncApiMockServer` also reject private network addresses, including localhost. Load local files or pass preloaded document content for local development. Docker documents supplied through `OPENAPI_DOCUMENT` resolve relative references from their temporary `/tmp/openapi.json` or `/tmp/openapi.yaml` file, confined to `/tmp`. Both OpenAPI and AsyncAPI Docker documents preserve the selected source origin.
|
|
18
|
+
|
|
19
|
+
- [#10192](https://github.com/scalar/scalar/pull/10192): Apply edited XML bodies instead of their original serialized or data examples, render schema-free XML examples in Markdown, and share reference decoding and response provenance selection.
|
|
20
|
+
|
|
21
|
+
## 0.16.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- [#10189](https://github.com/scalar/scalar/pull/10189): Support OpenAPI 3.2 `in: querystring` parameters in request validation and custom handlers. Decode JSON, text, and form content from the entire query string, including inherited parameters and form property encoding.
|
|
26
|
+
|
|
27
|
+
Treat only null or undefined as absent validator schemas, preserving boolean `false` whole-query schemas that reject every value. Empty object schemas were already compiled and continue to accept unconstrained values.
|
|
28
|
+
|
|
29
|
+
- [#10186](https://github.com/scalar/scalar/pull/10186): Support additionalOperations with case-sensitive custom HTTP methods, existing operation middleware, and CORS preflight responses.
|
|
30
|
+
|
|
31
|
+
Preserve QUERY in the default CORS method list when adding methods declared by the API description.
|
|
32
|
+
|
|
3
33
|
## 0.15.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
|
@@ -9,6 +9,8 @@ export type AsyncApiMockServerOptions = {
|
|
|
9
9
|
* string, or an already-parsed object.
|
|
10
10
|
*/
|
|
11
11
|
document?: string | Record<string, any>;
|
|
12
|
+
/** Source file path or URL for resolving references in an already loaded document. */
|
|
13
|
+
origin?: string;
|
|
12
14
|
/**
|
|
13
15
|
* Additional transports appended after the built-in WebSocket and SSE transports. Use this to
|
|
14
16
|
* support extra protocols (for example SignalR) without changing the core. The first transport
|
|
@@ -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,KAAK,mBAAmB,EAAoB,MAAM,mBAAmB,CAAA;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAK3B,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,wFAAwF;IACxF,SAAS,EAAE;QAAE,MAAM,EAAE,mBAAmB,CAAA;KAAE,CAAA;CAC3C,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqC9G"}
|
|
1
|
+
{"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAoB,MAAM,mBAAmB,CAAA;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAK3B,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,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;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,wFAAwF;IACxF,SAAS,EAAE;QAAE,MAAM,EAAE,mBAAmB,CAAA;KAAE,CAAA;CAC3C,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqC9G"}
|
|
@@ -22,7 +22,7 @@ import { resolveLogger } from './utils/resolve-logger.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export async function createAsyncApiMockServer(options) {
|
|
24
24
|
const app = new Hono();
|
|
25
|
-
const document = await processAsyncApiDocument(options.document);
|
|
25
|
+
const document = await processAsyncApiDocument(options.document, options.origin);
|
|
26
26
|
const channels = resolveChannels(document);
|
|
27
27
|
const transports = [...defaultTransports, ...(options.transports ?? [])];
|
|
28
28
|
const log = resolveLogger(options.logger, false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,IAAI,EAA0B,MAAM,MAAM,CAAA;AAIjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAwDhD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwOtF"}
|
|
@@ -103,7 +103,13 @@ export async function createMockServer(configuration) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
// CORS headers
|
|
106
|
-
|
|
106
|
+
const allowedMethods = new Set(['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'QUERY']);
|
|
107
|
+
for (const pathItem of Object.values(schema?.paths ?? {})) {
|
|
108
|
+
for (const method of Object.keys(getOperations(getResolvedRef(pathItem)))) {
|
|
109
|
+
allowedMethods.add(method);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
app.use(cors({ origin: '*', allowMethods: [...allowedMethods], exposeHeaders: ['X-Scalar-XML-Error'] }));
|
|
107
113
|
/** Authentication methods defined in the OpenAPI document */
|
|
108
114
|
setUpAuthenticationRoutes(app, schema);
|
|
109
115
|
// Only the instructions honor `logger` (on by default); the util still prints warnings and errors
|
|
@@ -136,19 +142,17 @@ export async function createMockServer(configuration) {
|
|
|
136
142
|
orderedPathKeys.forEach(({ path, query }) => {
|
|
137
143
|
// A path item may itself be a `$ref`, so resolve it before reading its operations.
|
|
138
144
|
const pathItem = getResolvedRef(paths[path]);
|
|
139
|
-
const
|
|
145
|
+
const operations = getOperations(pathItem);
|
|
140
146
|
/** Keys for all operations of a specified path */
|
|
141
|
-
|
|
147
|
+
Object.entries(operations).forEach(([method, operation]) => {
|
|
142
148
|
const route = honoRouteFromPath(path);
|
|
143
|
-
const operation = pathItem?.[method];
|
|
144
149
|
// Remember which operation this route mocks, so the error handler can name it when something
|
|
145
150
|
// fails downstream. Recorded on the context rather than mapped back from the request path,
|
|
146
151
|
// which would not survive the app being mounted under a base path. Registered before the rest
|
|
147
152
|
// of the route so a failure in request validation is named too. The OpenAPI path key is kept
|
|
148
153
|
// (rather than the Hono route) because that is what the document author reads.
|
|
149
154
|
const mockedOperation = {
|
|
150
|
-
|
|
151
|
-
method: method.toUpperCase(),
|
|
155
|
+
method,
|
|
152
156
|
path,
|
|
153
157
|
...(operation?.operationId ? { operationId: operation.operationId } : {}),
|
|
154
158
|
};
|
|
@@ -188,21 +192,28 @@ export async function createMockServer(configuration) {
|
|
|
188
192
|
const hasHandler = handlerCode && typeof handlerCode === 'string' && handlerCode.trim().length > 0;
|
|
189
193
|
// Route to appropriate handler
|
|
190
194
|
if (hasHandler) {
|
|
191
|
-
handlers.push(async (c) => await mockHandlerResponse(c, operation));
|
|
195
|
+
handlers.push(async (c) => await mockHandlerResponse(c, operation, pathItem?.parameters, schema.openapi));
|
|
192
196
|
}
|
|
193
197
|
else {
|
|
194
|
-
handlers.push(async (c) => await mockAnyResponse(c, operation));
|
|
195
|
-
}
|
|
196
|
-
if (query.length === 0) {
|
|
197
|
-
handlers.forEach((handler) => app[method](route, handler));
|
|
198
|
-
return;
|
|
198
|
+
handlers.push(async (c) => await mockAnyResponse(c, operation, schema.openapi));
|
|
199
199
|
}
|
|
200
200
|
// The pinned query parameters are not part of the route, so they are checked here. A request
|
|
201
201
|
// that does not carry them is handed on to the next matching route — usually the sibling path
|
|
202
202
|
// key without the query string.
|
|
203
203
|
const operationChain = every(...handlers);
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
// Hono uppercases methods during registration. Match the original method ourselves so
|
|
205
|
+
// additional operations such as COPY and copy remain distinct.
|
|
206
|
+
const register = (handler) => {
|
|
207
|
+
if (method === method.toUpperCase()) {
|
|
208
|
+
app.on(method, route, handler);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
app.all(route, handler);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
register(async (c, next) => {
|
|
215
|
+
if ((c.req.method !== method && !(method === 'GET' && c.req.method === 'HEAD')) ||
|
|
216
|
+
!requestMatchesPinnedQuery(c, query)) {
|
|
206
217
|
await next();
|
|
207
218
|
return;
|
|
208
219
|
}
|
|
@@ -3,5 +3,5 @@ import type { Context } from 'hono';
|
|
|
3
3
|
/**
|
|
4
4
|
* Mock any response
|
|
5
5
|
*/
|
|
6
|
-
export declare function mockAnyResponse(c: Context, operation: OpenAPIV3_1.OperationObject): Response;
|
|
6
|
+
export declare function mockAnyResponse(c: Context, operation: OpenAPIV3_1.OperationObject, openapiVersion?: string): Response;
|
|
7
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":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAKxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAenC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,EAAE,cAAc,CAAC,EAAE,MAAM,YA4J1G"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
1
2
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
3
|
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
3
|
-
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
4
|
+
import { getExampleFromSchema, getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
4
5
|
import { streamSSE } from 'hono/streaming';
|
|
5
6
|
import { collectSseEvents, isEventStreamContentType } from '../utils/collect-sse-events.js';
|
|
6
7
|
import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
|
|
@@ -15,7 +16,7 @@ import { getStreamingResponse, sendStreamingResponse } from '../utils/streaming-
|
|
|
15
16
|
/**
|
|
16
17
|
* Mock any response
|
|
17
18
|
*/
|
|
18
|
-
export function mockAnyResponse(c, operation) {
|
|
19
|
+
export function mockAnyResponse(c, operation, openapiVersion) {
|
|
19
20
|
// Note: the `onRequest` callback runs as middleware (see `create-mock-server`) so it also fires
|
|
20
21
|
// for requests rejected before reaching this handler.
|
|
21
22
|
// Parse the Prefer header (RFC 7240) so clients can request a specific
|
|
@@ -102,9 +103,24 @@ export function mockAnyResponse(c, operation) {
|
|
|
102
103
|
// Body: a named/singular/first example if one is defined, otherwise generate
|
|
103
104
|
// a value from the schema. `Prefer: example=<name>` picks a named example.
|
|
104
105
|
const selectedExample = selectResponseExample(acceptedResponse, prefer.example);
|
|
106
|
+
c.status(statusCode);
|
|
107
|
+
if (isXmlMediaType(acceptedContentType)) {
|
|
108
|
+
const result = getXmlBodyExample(acceptedResponse?.schema, selectedExample, {
|
|
109
|
+
openapiVersion,
|
|
110
|
+
emptyString: 'string',
|
|
111
|
+
variables: pathParameters(c),
|
|
112
|
+
mode: 'read',
|
|
113
|
+
});
|
|
114
|
+
const error = result.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
115
|
+
if (error) {
|
|
116
|
+
c.header('X-Scalar-XML-Error', error.code);
|
|
117
|
+
}
|
|
118
|
+
return result.xml === undefined ? c.body(null) : c.body(result.xml);
|
|
119
|
+
}
|
|
120
|
+
const provenance = selectedExample?.provenance;
|
|
105
121
|
const body = (() => {
|
|
106
122
|
if (selectedExample) {
|
|
107
|
-
return normalizeResponseBody(selectedExample.value, responseSchema);
|
|
123
|
+
return provenance ? selectedExample.value : normalizeResponseBody(selectedExample.value, responseSchema);
|
|
108
124
|
}
|
|
109
125
|
if (!responseSchema) {
|
|
110
126
|
return null;
|
|
@@ -119,8 +135,7 @@ export function mockAnyResponse(c, operation) {
|
|
|
119
135
|
// Re-inferring it from sibling items would wrap a selected primitive in an array.
|
|
120
136
|
return generated;
|
|
121
137
|
})();
|
|
122
|
-
|
|
123
|
-
const serializedBody = serializeResponseBody(body, acceptedContentType, responseSchema);
|
|
138
|
+
const serializedBody = serializeResponseBody(body, acceptedContentType, responseSchema, provenance);
|
|
124
139
|
// `JSON.stringify` returns `undefined` for an `undefined` body, which is an empty response.
|
|
125
140
|
if (serializedBody === undefined) {
|
|
126
141
|
return c.body(null);
|
|
@@ -4,5 +4,5 @@ import type { Context } from 'hono';
|
|
|
4
4
|
* Mock response using x-handler code.
|
|
5
5
|
* Executes the handler and returns its result as the response.
|
|
6
6
|
*/
|
|
7
|
-
export declare function mockHandlerResponse(c: Context, operation: OpenAPIV3_1.OperationObject): Promise<Response>;
|
|
7
|
+
export declare function mockHandlerResponse(c: Context, operation: OpenAPIV3_1.OperationObject, pathItemParameters?: OpenAPIV3_1.PathItemObject['parameters'], openapiVersion?: string): Promise<Response>;
|
|
8
8
|
//# sourceMappingURL=mock-handler-response.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAoJnC;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,WAAW,CAAC,eAAe,EACtC,kBAAkB,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,EAC7D,cAAc,CAAC,EAAE,MAAM,qBA8ExB"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
1
2
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
|
+
import { getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
2
4
|
import { buildHandlerContext } from '../utils/build-handler-context.js';
|
|
3
5
|
import { executeHandler } from '../utils/execute-handler.js';
|
|
4
6
|
import { generateResponseExample } from '../utils/generate-response-example.js';
|
|
@@ -7,16 +9,17 @@ import { normalizeResponseBody } from '../utils/normalize-response-body.js';
|
|
|
7
9
|
import { parsePreferHeader } from '../utils/parse-prefer-header.js';
|
|
8
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
|
import { getStreamingResponse, sendStreamingResponse } from '../utils/streaming-response.js';
|
|
11
14
|
/**
|
|
12
15
|
* Get example response from OpenAPI spec for a given status code.
|
|
13
|
-
* Returns
|
|
16
|
+
* Returns a serialized payload if found, or null if not available.
|
|
14
17
|
*
|
|
15
18
|
* Honors `Prefer: example=<name>` to pick a named example from the
|
|
16
19
|
* `examples` map; otherwise it falls back to the singular `example`, the
|
|
17
20
|
* first entry of the map, or a value generated from the schema.
|
|
18
21
|
*/
|
|
19
|
-
function getExampleFromResponse(c, statusCode, responses, exampleName) {
|
|
22
|
+
function getExampleFromResponse(c, statusCode, responses, exampleName, openapiVersion) {
|
|
20
23
|
if (!responses) {
|
|
21
24
|
return null;
|
|
22
25
|
}
|
|
@@ -39,11 +42,32 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
|
|
|
39
42
|
const responseSchema = acceptedResponse.schema ? getResolvedRef(acceptedResponse.schema) : undefined;
|
|
40
43
|
// Extract example (named, singular, or first) or generate from schema
|
|
41
44
|
const selectedExample = selectResponseExample(acceptedResponse, exampleName);
|
|
42
|
-
|
|
45
|
+
if (isXmlMediaType(acceptedContentType)) {
|
|
46
|
+
c.header('Content-Type', acceptedContentType);
|
|
47
|
+
const result = getXmlBodyExample(acceptedResponse.schema, selectedExample, {
|
|
48
|
+
openapiVersion,
|
|
49
|
+
emptyString: 'string',
|
|
50
|
+
variables: pathParameters(c),
|
|
51
|
+
mode: 'read',
|
|
52
|
+
});
|
|
53
|
+
const error = result.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
54
|
+
if (error) {
|
|
55
|
+
c.header('X-Scalar-XML-Error', error.code);
|
|
56
|
+
}
|
|
57
|
+
return result.xml ?? null;
|
|
58
|
+
}
|
|
59
|
+
const provenance = selectedExample?.provenance;
|
|
60
|
+
if (selectedExample && provenance) {
|
|
61
|
+
c.header('Content-Type', acceptedContentType);
|
|
62
|
+
return serializeResponseBody(selectedExample.value, acceptedContentType, responseSchema, provenance) ?? null;
|
|
63
|
+
}
|
|
64
|
+
const value = selectedExample
|
|
43
65
|
? normalizeResponseBody(selectedExample.value, responseSchema)
|
|
44
66
|
: responseSchema
|
|
45
67
|
? normalizeResponseBody(generateResponseExample(responseSchema, pathParameters(c)), responseSchema)
|
|
46
68
|
: null;
|
|
69
|
+
// Legacy examples retain the handler fallback's JSON encoding policy.
|
|
70
|
+
return JSON.stringify(value) ?? null;
|
|
47
71
|
}
|
|
48
72
|
/**
|
|
49
73
|
* Determine HTTP status code based on store operation tracking.
|
|
@@ -98,7 +122,7 @@ function determineStatusCode(tracking) {
|
|
|
98
122
|
* Mock response using x-handler code.
|
|
99
123
|
* Executes the handler and returns its result as the response.
|
|
100
124
|
*/
|
|
101
|
-
export async function mockHandlerResponse(c, operation) {
|
|
125
|
+
export async function mockHandlerResponse(c, operation, pathItemParameters, openapiVersion) {
|
|
102
126
|
// Note: the `onRequest` callback runs as middleware (see `create-mock-server`) so it also fires
|
|
103
127
|
// for requests rejected before reaching this handler.
|
|
104
128
|
// Get x-handler code from operation
|
|
@@ -109,7 +133,7 @@ export async function mockHandlerResponse(c, operation) {
|
|
|
109
133
|
}
|
|
110
134
|
try {
|
|
111
135
|
// Build handler context with tracking
|
|
112
|
-
const { context, tracking } = await buildHandlerContext(c, operation);
|
|
136
|
+
const { context, tracking } = await buildHandlerContext(c, operation, pathItemParameters);
|
|
113
137
|
// Execute handler
|
|
114
138
|
const { result } = await executeHandler(handlerCode, context);
|
|
115
139
|
// Determine status code based on all store operations, prioritizing semantically meaningful ones
|
|
@@ -137,9 +161,12 @@ export async function mockHandlerResponse(c, operation) {
|
|
|
137
161
|
if (result === undefined || result === null) {
|
|
138
162
|
// Try to pick up example response from OpenAPI spec if available
|
|
139
163
|
const prefer = parsePreferHeader(c.req.header('Prefer'));
|
|
140
|
-
const exampleResponse = getExampleFromResponse(c, statusCode, operation.responses, prefer.example);
|
|
164
|
+
const exampleResponse = getExampleFromResponse(c, statusCode, operation.responses, prefer.example, openapiVersion);
|
|
141
165
|
if (exampleResponse !== null) {
|
|
142
|
-
return c.
|
|
166
|
+
return c.body(exampleResponse);
|
|
167
|
+
}
|
|
168
|
+
if (isXmlMediaType(c.res.headers.get('Content-Type') ?? undefined)) {
|
|
169
|
+
return c.body(null);
|
|
143
170
|
}
|
|
144
171
|
return c.json(null);
|
|
145
172
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,8 +2,6 @@ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
|
2
2
|
import type { Context } from 'hono';
|
|
3
3
|
/** Available HTTP methods for Hono routes */
|
|
4
4
|
export declare const httpMethods: readonly ["get", "put", "post", "delete", "options", "patch"];
|
|
5
|
-
/** Valid HTTP method */
|
|
6
|
-
export type HttpMethod = (typeof httpMethods)[number];
|
|
7
5
|
/**
|
|
8
6
|
* Represents a partial object where at least one of the given properties is required.
|
|
9
7
|
*/
|
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
|
|
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;;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,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;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"}
|
|
@@ -9,7 +9,8 @@ export type HandlerContext = {
|
|
|
9
9
|
req: {
|
|
10
10
|
body: any;
|
|
11
11
|
params: Record<string, string>;
|
|
12
|
-
query
|
|
12
|
+
/** Named query values, or the decoded content of an `in: querystring` parameter. */
|
|
13
|
+
query: unknown;
|
|
13
14
|
headers: Record<string, string>;
|
|
14
15
|
};
|
|
15
16
|
res: Record<string, any>;
|
|
@@ -24,6 +25,6 @@ type HandlerContextResult = {
|
|
|
24
25
|
/**
|
|
25
26
|
* Build the handler context from a Hono context.
|
|
26
27
|
*/
|
|
27
|
-
export declare function buildHandlerContext(c: Context, operation?: OpenAPIV3_1.OperationObject): Promise<HandlerContextResult>;
|
|
28
|
+
export declare function buildHandlerContext(c: Context, operation?: OpenAPIV3_1.OperationObject, pathItemParameters?: OpenAPIV3_1.PathItemObject['parameters']): Promise<HandlerContextResult>;
|
|
28
29
|
export {};
|
|
29
30
|
//# sourceMappingURL=build-handler-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,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,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AASnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,oFAAoF;QACpF,KAAK,EAAE,OAAO,CAAA;QACd,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;AA6DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,EACvC,kBAAkB,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,GAC5D,OAAO,CAAC,oBAAoB,CAAC,CAuD/B"}
|
|
@@ -6,6 +6,7 @@ import { generateResponseExample } from './generate-response-example.js';
|
|
|
6
6
|
import { normalizeResponseBody } from './normalize-response-body.js';
|
|
7
7
|
import { parsePreferHeader } from './parse-prefer-header.js';
|
|
8
8
|
import { pathParameters } from './path-parameters.js';
|
|
9
|
+
import { findQuerystringParameter, parseQuerystringParameter } from './querystring-parameter.js';
|
|
9
10
|
import { createStoreWrapper } from './store-wrapper.js';
|
|
10
11
|
import { getStreamingResponse } from './streaming-response.js';
|
|
11
12
|
/**
|
|
@@ -55,7 +56,7 @@ function getExampleFromResponse(c, statusCode, responses) {
|
|
|
55
56
|
/**
|
|
56
57
|
* Build the handler context from a Hono context.
|
|
57
58
|
*/
|
|
58
|
-
export async function buildHandlerContext(c, operation) {
|
|
59
|
+
export async function buildHandlerContext(c, operation, pathItemParameters) {
|
|
59
60
|
let body = undefined;
|
|
60
61
|
try {
|
|
61
62
|
// Compare case-insensitively, since media types are case-insensitive and request validation
|
|
@@ -77,6 +78,17 @@ export async function buildHandlerContext(c, operation) {
|
|
|
77
78
|
catch {
|
|
78
79
|
// Ignore parsing errors, body remains undefined
|
|
79
80
|
}
|
|
81
|
+
const parameter = findQuerystringParameter(operation, pathItemParameters);
|
|
82
|
+
let query = Object.fromEntries(new URL(c.req.url).searchParams.entries());
|
|
83
|
+
if (parameter) {
|
|
84
|
+
try {
|
|
85
|
+
query = parseQuerystringParameter(c.req.url, parameter);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// With validation disabled, malformed content remains unavailable just like an invalid body.
|
|
89
|
+
query = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
80
92
|
const { wrappedStore, tracking } = createStoreWrapper(store);
|
|
81
93
|
// Build res object with examples for all response status codes
|
|
82
94
|
const res = {};
|
|
@@ -91,7 +103,7 @@ export async function buildHandlerContext(c, operation) {
|
|
|
91
103
|
req: {
|
|
92
104
|
body,
|
|
93
105
|
params: pathParameters(c),
|
|
94
|
-
query
|
|
106
|
+
query,
|
|
95
107
|
headers: Object.fromEntries(Object.entries(c.req.header()).map(([key, value]) => [key, value ?? ''])),
|
|
96
108
|
},
|
|
97
109
|
res,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
2
|
-
import { type HttpMethod } from '../types.js';
|
|
3
2
|
/**
|
|
4
3
|
* Takes a dereferenced OpenAPI document and returns all operations.
|
|
4
|
+
* Keys use their wire capitalization: fixed methods are uppercase, additional methods retain their case.
|
|
5
5
|
* Ignores other attributes, like summary, parameters, etc.
|
|
6
6
|
*/
|
|
7
|
-
export declare
|
|
7
|
+
export declare const getOperations: (path?: OpenAPIV3_1.PathItemObject & {
|
|
8
|
+
additionalOperations?: Record<string, OpenAPIV3_1.OperationObject>;
|
|
9
|
+
}) => Record<string, OpenAPIV3_1.OperationObject>;
|
|
8
10
|
//# sourceMappingURL=get-operation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-operation.d.ts","sourceRoot":"","sources":["../../src/utils/get-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"get-operation.d.ts","sourceRoot":"","sources":["../../src/utils/get-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,OAAO,WAAW,CAAC,cAAc,GAAG;IAClC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,eAAe,CAAC,CAAA;CACnE,KACA,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,eAAe,CAc5C,CAAA"}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { httpMethods } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Takes a dereferenced OpenAPI document and returns all operations.
|
|
4
|
+
* Keys use their wire capitalization: fixed methods are uppercase, additional methods retain their case.
|
|
4
5
|
* Ignores other attributes, like summary, parameters, etc.
|
|
5
6
|
*/
|
|
6
|
-
export
|
|
7
|
-
const operations =
|
|
7
|
+
export const getOperations = (path) => {
|
|
8
|
+
const operations = new Map();
|
|
8
9
|
for (const method of httpMethods) {
|
|
9
10
|
if (path?.[method]) {
|
|
10
|
-
operations
|
|
11
|
+
operations.set(method.toUpperCase(), path[method]);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
for (const [method, operation] of Object.entries(path?.additionalOperations ?? {})) {
|
|
15
|
+
operations.set(method, operation);
|
|
16
|
+
}
|
|
17
|
+
return Object.fromEntries(operations);
|
|
18
|
+
};
|
|
@@ -11,10 +11,11 @@ import type { AsyncApiDocument } from '@scalar/types/asyncapi/3.1';
|
|
|
11
11
|
* Only AsyncAPI 3.1 is supported; 2.x documents should be upgraded before being passed in.
|
|
12
12
|
*
|
|
13
13
|
* @param document - The AsyncAPI document to process. Can be a string (URL/path) or an object.
|
|
14
|
+
* @param origin - Source file path or URL for resolving references in an already loaded document.
|
|
14
15
|
* @returns A promise that resolves to the AsyncAPI document with lazily resolvable references.
|
|
15
16
|
* @throws Error if the document cannot be processed or is invalid.
|
|
16
17
|
*/
|
|
17
|
-
export declare function processAsyncApiDocument(document: string | Record<string, any> | undefined): Promise<AsyncApiDocument>;
|
|
18
|
+
export declare function processAsyncApiDocument(document: string | Record<string, any> | undefined, origin?: string): Promise<AsyncApiDocument>;
|
|
18
19
|
/**
|
|
19
20
|
* Detects whether a loaded document describes an AsyncAPI API (rather than OpenAPI/Swagger).
|
|
20
21
|
* Used to route an incoming document to the right mock engine.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-asyncapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-asyncapi-document.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"process-asyncapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-asyncapi-document.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAClD,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,CAAC,CAsC3B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAE7D"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { cwd } from 'node:process';
|
|
1
3
|
import { bundle } from '@scalar/json-magic/bundle';
|
|
2
4
|
import { fetchUrls, parseJson, parseYaml, readFiles } from '@scalar/json-magic/bundle/plugins/node';
|
|
5
|
+
import { isFilePath } from '@scalar/json-magic/helpers/is-file-path';
|
|
3
6
|
import { createMagicProxy } from '@scalar/json-magic/magic-proxy';
|
|
4
7
|
/**
|
|
5
8
|
* Processes an AsyncAPI document by bundling external references and wrapping it so internal
|
|
@@ -13,10 +16,11 @@ import { createMagicProxy } from '@scalar/json-magic/magic-proxy';
|
|
|
13
16
|
* Only AsyncAPI 3.1 is supported; 2.x documents should be upgraded before being passed in.
|
|
14
17
|
*
|
|
15
18
|
* @param document - The AsyncAPI document to process. Can be a string (URL/path) or an object.
|
|
19
|
+
* @param origin - Source file path or URL for resolving references in an already loaded document.
|
|
16
20
|
* @returns A promise that resolves to the AsyncAPI document with lazily resolvable references.
|
|
17
21
|
* @throws Error if the document cannot be processed or is invalid.
|
|
18
22
|
*/
|
|
19
|
-
export async function processAsyncApiDocument(document) {
|
|
23
|
+
export async function processAsyncApiDocument(document, origin) {
|
|
20
24
|
// Handle empty/undefined input gracefully with a minimal valid document.
|
|
21
25
|
if (!document || (typeof document === 'object' && Object.keys(document).length === 0)) {
|
|
22
26
|
return {
|
|
@@ -30,10 +34,14 @@ export async function processAsyncApiDocument(document) {
|
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
let bundled;
|
|
37
|
+
// Keep references inside the source directory and prevent access to internal services.
|
|
38
|
+
const source = origin ?? document;
|
|
39
|
+
const basePath = typeof source === 'string' && isFilePath(source) ? path.dirname(path.resolve(source)) : cwd();
|
|
33
40
|
try {
|
|
34
41
|
// Bundle external references; parse string inputs (JSON or YAML) along the way.
|
|
35
42
|
bundled = await bundle(document, {
|
|
36
|
-
|
|
43
|
+
origin,
|
|
44
|
+
plugins: [parseJson(), parseYaml(), readFiles({ basePath }), fetchUrls({ blockPrivateNetworks: true })],
|
|
37
45
|
treeShake: false,
|
|
38
46
|
});
|
|
39
47
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OpenAPIV3_2 } from '@scalar/openapi-types';
|
|
2
|
+
/**
|
|
3
|
+
* Find the whole-query parameter, giving operation declarations precedence over path declarations.
|
|
4
|
+
* This module decodes incoming queries; workspace-store/src/helpers/querystring-parameter.ts serializes outgoing ones.
|
|
5
|
+
*/
|
|
6
|
+
export declare const findQuerystringParameter: (operation?: OpenAPIV3_2.OperationObject, pathParameters?: OpenAPIV3_2.PathItemObject["parameters"]) => OpenAPIV3_2.ParameterObject | undefined;
|
|
7
|
+
/** Validate JSON-encoded form properties before the coercing form validator can alter their native types. */
|
|
8
|
+
export declare const getQuerystringJsonSchema: (parameter: OpenAPIV3_2.ParameterObject | undefined) => Record<string, unknown> | null;
|
|
9
|
+
/** Decode the complete query without introducing the parameter's documentary name. Throws on malformed JSON or URI escaping. */
|
|
10
|
+
export declare const parseQuerystringParameter: (url: string, parameter: OpenAPIV3_2.ParameterObject) => unknown;
|
|
11
|
+
//# sourceMappingURL=querystring-parameter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"querystring-parameter.d.ts","sourceRoot":"","sources":["../../src/utils/querystring-parameter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAaxD;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,YAAY,WAAW,CAAC,eAAe,EACvC,iBAAiB,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,KACxD,WAAW,CAAC,eAAe,GAAG,SAMwB,CAAA;AAkBzD,6GAA6G;AAC7G,eAAO,MAAM,wBAAwB,GACnC,WAAW,WAAW,CAAC,eAAe,GAAG,SAAS,KACjD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAkC5B,CAAA;AAED,gIAAgI;AAChI,eAAO,MAAM,yBAAyB,GAAI,KAAK,MAAM,EAAE,WAAW,WAAW,CAAC,eAAe,KAAG,OA0E/F,CAAA"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { getFirstMediaType } from '@scalar/helpers/http/get-first-media-type';
|
|
2
|
+
import { isJsonMediaType } from '@scalar/helpers/http/is-json-media-type';
|
|
3
|
+
import { parseMimeType } from '@scalar/helpers/http/mime-type';
|
|
4
|
+
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
5
|
+
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
6
|
+
import { deserializeArrayParameter, deserializeObjectParameter, getObjectPropertyNames, isArraySchema, isObjectSchema, resolveSerialization, } from './deserialize-parameter.js';
|
|
7
|
+
/**
|
|
8
|
+
* Find the whole-query parameter, giving operation declarations precedence over path declarations.
|
|
9
|
+
* This module decodes incoming queries; workspace-store/src/helpers/querystring-parameter.ts serializes outgoing ones.
|
|
10
|
+
*/
|
|
11
|
+
export const findQuerystringParameter = (operation, pathParameters) => [
|
|
12
|
+
...(Array.isArray(operation?.parameters) ? operation.parameters : []),
|
|
13
|
+
...(Array.isArray(pathParameters) ? pathParameters : []),
|
|
14
|
+
]
|
|
15
|
+
.map((parameter) => getResolvedRef(parameter))
|
|
16
|
+
.find((parameter) => parameter?.in === 'querystring');
|
|
17
|
+
/** Collect form properties through compositions so nullable and composed objects retain their encoding. */
|
|
18
|
+
const getPropertySchemas = (schema) => {
|
|
19
|
+
const properties = {};
|
|
20
|
+
for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
|
|
21
|
+
const branches = schema?.[keyword];
|
|
22
|
+
if (Array.isArray(branches)) {
|
|
23
|
+
for (const branch of branches) {
|
|
24
|
+
if (branch && typeof branch === 'object') {
|
|
25
|
+
Object.assign(properties, getPropertySchemas(branch));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { ...properties, ...schema?.properties };
|
|
31
|
+
};
|
|
32
|
+
/** Validate JSON-encoded form properties before the coercing form validator can alter their native types. */
|
|
33
|
+
export const getQuerystringJsonSchema = (parameter) => {
|
|
34
|
+
const [contentType, media] = getFirstMediaType(parameter?.content) ?? [];
|
|
35
|
+
if (parseMimeType(contentType).essence !== 'application/x-www-form-urlencoded') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const selectJsonProperties = (schema) => {
|
|
39
|
+
const properties = (schema?.properties ?? {});
|
|
40
|
+
const jsonProperties = Object.fromEntries(Object.entries(properties).filter(([name, property]) => {
|
|
41
|
+
const encoding = media?.encoding?.[name];
|
|
42
|
+
if (encoding?.style !== undefined || encoding?.explode !== undefined || encoding?.allowReserved !== undefined) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const item = isArraySchema(property) ? property.items : property;
|
|
46
|
+
return isJsonMediaType(encoding?.contentType) || isObjectSchema(item) || isArraySchema(item);
|
|
47
|
+
}));
|
|
48
|
+
const result = { properties: jsonProperties };
|
|
49
|
+
for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
|
|
50
|
+
const branches = schema?.[keyword];
|
|
51
|
+
if (Array.isArray(branches)) {
|
|
52
|
+
// Non-JSON fields may distinguish oneOf branches. The complete validator enforces exclusivity.
|
|
53
|
+
const target = keyword === 'oneOf' ? 'anyOf' : keyword;
|
|
54
|
+
const selected = branches.map((branch) => selectJsonProperties(branch));
|
|
55
|
+
if (target in result) {
|
|
56
|
+
result.allOf = [...(Array.isArray(result.allOf) ? result.allOf : []), { [target]: selected }];
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
result[target] = selected;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
return selectJsonProperties(getResolvedRefDeep(media?.schema));
|
|
66
|
+
};
|
|
67
|
+
/** Decode the complete query without introducing the parameter's documentary name. Throws on malformed JSON or URI escaping. */
|
|
68
|
+
export const parseQuerystringParameter = (url, parameter) => {
|
|
69
|
+
const query = new URL(url).search.slice(1);
|
|
70
|
+
if (!query) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const [contentType, media] = getFirstMediaType(parameter.content) ?? [];
|
|
74
|
+
const mediaType = parseMimeType(contentType).essence;
|
|
75
|
+
if (mediaType !== 'application/x-www-form-urlencoded') {
|
|
76
|
+
const decoded = decodeURIComponent(query);
|
|
77
|
+
return isJsonMediaType(mediaType) ? JSON.parse(decoded) : decoded;
|
|
78
|
+
}
|
|
79
|
+
const params = new URLSearchParams(query);
|
|
80
|
+
const map = Object.fromEntries([...new Set(params.keys())].map((key) => {
|
|
81
|
+
const values = params.getAll(key);
|
|
82
|
+
return [key, values.length === 1 ? (values[0] ?? '') : values];
|
|
83
|
+
}));
|
|
84
|
+
const result = { ...map };
|
|
85
|
+
const schema = getResolvedRefDeep(media?.schema);
|
|
86
|
+
const properties = getPropertySchemas(schema);
|
|
87
|
+
for (const name of getObjectPropertyNames(schema)) {
|
|
88
|
+
const property = properties?.[name];
|
|
89
|
+
const encoding = media?.encoding?.[name];
|
|
90
|
+
const single = params.get(name) ?? undefined;
|
|
91
|
+
const styleBased = encoding?.style !== undefined || encoding?.explode !== undefined || encoding?.allowReserved !== undefined;
|
|
92
|
+
if (styleBased) {
|
|
93
|
+
const { style, explode } = resolveSerialization('query', encoding?.style, encoding?.explode);
|
|
94
|
+
const value = isObjectSchema(property)
|
|
95
|
+
? deserializeObjectParameter({
|
|
96
|
+
style,
|
|
97
|
+
explode,
|
|
98
|
+
single,
|
|
99
|
+
map,
|
|
100
|
+
name,
|
|
101
|
+
propertyNames: getObjectPropertyNames(property),
|
|
102
|
+
reservedKeys: new Set(Object.keys(properties)),
|
|
103
|
+
})
|
|
104
|
+
: isArraySchema(property)
|
|
105
|
+
? deserializeArrayParameter({
|
|
106
|
+
style,
|
|
107
|
+
explode,
|
|
108
|
+
single,
|
|
109
|
+
multi: params.has(name) ? params.getAll(name) : undefined,
|
|
110
|
+
})
|
|
111
|
+
: single;
|
|
112
|
+
if (value !== undefined) {
|
|
113
|
+
if (isObjectSchema(property) &&
|
|
114
|
+
(style === 'deepObject' || (style === 'form' && explode)) &&
|
|
115
|
+
typeof value === 'object' &&
|
|
116
|
+
value !== null) {
|
|
117
|
+
for (const key of Object.keys(value)) {
|
|
118
|
+
delete result[style === 'deepObject' ? `${name}[${key}]` : key];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result[name] = value;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (single === undefined) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const json = isJsonMediaType(encoding?.contentType);
|
|
129
|
+
const decode = (value, itemSchema) => json || isObjectSchema(itemSchema) || isArraySchema(itemSchema) ? JSON.parse(value) : value;
|
|
130
|
+
result[name] = isArraySchema(property)
|
|
131
|
+
? params.getAll(name).map((value) => decode(value, property?.items))
|
|
132
|
+
: decode(single, property);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
2
|
+
import type { ExampleObject } from '@scalar/workspace-store/schemas/v3.2/strict/example';
|
|
3
|
+
type SelectedResponseExample = ExampleObject & {
|
|
4
|
+
value: unknown;
|
|
5
|
+
provenance?: 'serialized' | 'data';
|
|
6
|
+
};
|
|
2
7
|
/**
|
|
3
8
|
* Pick the example body for a response media type.
|
|
4
9
|
*
|
|
@@ -18,7 +23,6 @@ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
|
18
23
|
*/
|
|
19
24
|
export declare const selectResponseExample: <T extends Pick<OpenAPIV3_1.MediaTypeObject, "example" | "examples"> & {
|
|
20
25
|
schema?: unknown;
|
|
21
|
-
}>(mediaType: T | undefined, exampleName?: string) =>
|
|
22
|
-
|
|
23
|
-
} | undefined;
|
|
26
|
+
}>(mediaType: T | undefined, exampleName?: string) => SelectedResponseExample | undefined;
|
|
27
|
+
export {};
|
|
24
28
|
//# sourceMappingURL=select-response-example.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-response-example.d.ts","sourceRoot":"","sources":["../../src/utils/select-response-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"select-response-example.d.ts","sourceRoot":"","sources":["../../src/utils/select-response-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAA;AAExF,KAAK,uBAAuB,GAAG,aAAa,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAA;CAAE,CAAA;AAarG;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GAChC,CAAC,SAAS,IAAI,CAAC,WAAW,CAAC,eAAe,EAAE,SAAS,GAAG,UAAU,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAE1F,WAAW,CAAC,GAAG,SAAS,EACxB,cAAc,MAAM,KACnB,uBAAuB,GAAG,SAkC5B,CAAA"}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
|
+
/** Keep serialized/data examples distinguishable until the media serializer runs. */
|
|
3
|
+
const exampleValue = (example) => {
|
|
4
|
+
if (example?.serializedValue !== undefined) {
|
|
5
|
+
return { serializedValue: example.serializedValue, value: example.serializedValue, provenance: 'serialized' };
|
|
6
|
+
}
|
|
7
|
+
if (example?.dataValue !== undefined) {
|
|
8
|
+
return { dataValue: example.dataValue, value: example.dataValue, provenance: 'data' };
|
|
9
|
+
}
|
|
10
|
+
return example?.value !== undefined ? { value: example.value } : undefined;
|
|
11
|
+
};
|
|
2
12
|
/**
|
|
3
13
|
* Pick the example body for a response media type.
|
|
4
14
|
*
|
|
@@ -23,9 +33,9 @@ export const selectResponseExample = (mediaType, exampleName) => {
|
|
|
23
33
|
const { example, examples } = mediaType;
|
|
24
34
|
// 1. A named example requested via `Prefer: example=<name>`
|
|
25
35
|
if (exampleName && examples && exampleName in examples) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
36
|
+
const selected = exampleValue(getResolvedRef(examples[exampleName]));
|
|
37
|
+
if (selected) {
|
|
38
|
+
return selected;
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
// 2. The singular `example` keyword
|
|
@@ -36,9 +46,9 @@ export const selectResponseExample = (mediaType, exampleName) => {
|
|
|
36
46
|
if (examples) {
|
|
37
47
|
const firstKey = Object.keys(examples)[0];
|
|
38
48
|
if (firstKey !== undefined) {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
49
|
+
const selected = exampleValue(getResolvedRef(examples[firstKey]));
|
|
50
|
+
if (selected) {
|
|
51
|
+
return selected;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
}
|
|
@@ -6,6 +6,6 @@ type Schema = NonNullable<OpenAPIV3_1.ComponentsObject['schemas']>[string];
|
|
|
6
6
|
* Returns `undefined` for an `undefined` body, mirroring `JSON.stringify`, so the caller can send an
|
|
7
7
|
* empty body rather than the characters `undefined`.
|
|
8
8
|
*/
|
|
9
|
-
export declare const serializeResponseBody: (body: unknown, contentType: string | undefined, schema?: Schema) => string | undefined;
|
|
9
|
+
export declare const serializeResponseBody: (body: unknown, contentType: string | undefined, schema?: Schema, provenance?: "data" | "serialized") => string | undefined;
|
|
10
10
|
export {};
|
|
11
11
|
//# sourceMappingURL=serialize-response-body.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialize-response-body.d.ts","sourceRoot":"","sources":["../../src/utils/serialize-response-body.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"serialize-response-body.d.ts","sourceRoot":"","sources":["../../src/utils/serialize-response-body.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAKxD,KAAK,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AA2D1E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,OAAO,EACb,aAAa,MAAM,GAAG,SAAS,EAC/B,SAAS,MAAM,EACf,aAAa,MAAM,GAAG,YAAY,KACjC,MAAM,GAAG,SAsCX,CAAA"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
2
2
|
import { parseMimeType } from '@scalar/helpers/http/mime-type';
|
|
3
|
+
import { serializeXmlExample } from '@scalar/workspace-store/request-example';
|
|
4
|
+
import { coerceValue } from '@scalar/workspace-store/schemas/typebox-coerce';
|
|
5
|
+
import { SchemaObjectSchema } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
3
6
|
/**
|
|
4
7
|
* Whether a media type carries a single JSON document.
|
|
5
8
|
*
|
|
@@ -13,11 +16,6 @@ const isJsonDocumentContentType = (contentType) => {
|
|
|
13
16
|
const { subtype } = parseMimeType(contentType);
|
|
14
17
|
return subtype === 'json' || subtype.endsWith('+json');
|
|
15
18
|
};
|
|
16
|
-
/** Whether a media type carries XML, including suffixed types such as `application/xhtml+xml`. */
|
|
17
|
-
const isXmlContentType = (contentType) => {
|
|
18
|
-
const { subtype } = parseMimeType(contentType);
|
|
19
|
-
return subtype === 'xml' || subtype.endsWith('+xml');
|
|
20
|
-
};
|
|
21
19
|
/**
|
|
22
20
|
* How the resolved response schema describes the body: as a string, as something else, or not at all.
|
|
23
21
|
*
|
|
@@ -58,11 +56,17 @@ const isSerializedJsonDocument = (value) => {
|
|
|
58
56
|
* Returns `undefined` for an `undefined` body, mirroring `JSON.stringify`, so the caller can send an
|
|
59
57
|
* empty body rather than the characters `undefined`.
|
|
60
58
|
*/
|
|
61
|
-
export const serializeResponseBody = (body, contentType, schema) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
export const serializeResponseBody = (body, contentType, schema, provenance) => {
|
|
60
|
+
if (provenance === 'serialized' && typeof body === 'string') {
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
if (provenance === 'data' && isJsonDocumentContentType(contentType)) {
|
|
64
|
+
return JSON.stringify(body);
|
|
65
|
+
}
|
|
66
|
+
if (isXmlMediaType(contentType)) {
|
|
67
|
+
return typeof body === 'string'
|
|
68
|
+
? body
|
|
69
|
+
: serializeXmlExample(body, coerceValue(SchemaObjectSchema, schema ?? {}), { mode: 'read' }).xml;
|
|
66
70
|
}
|
|
67
71
|
if (typeof body === 'string') {
|
|
68
72
|
// Anywhere but a single JSON document, the characters are the payload: `text/plain`, `text/html`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-request.d.ts","sourceRoot":"","sources":["../../src/utils/validate-request.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate-request.d.ts","sourceRoot":"","sources":["../../src/utils/validate-request.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,uBAAuB,CAAA;AAMrE,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAuTtD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,WAAW,CAAC,eAAe,EACtC,qBAAqB,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,KAC5D,iBAmLF,CAAA"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getFirstMediaType } from '@scalar/helpers/http/get-first-media-type';
|
|
2
|
+
import { parseMimeType } from '@scalar/helpers/http/mime-type';
|
|
1
3
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
4
|
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
3
5
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
6
|
import addFormats from 'ajv-formats';
|
|
5
7
|
import { getCookie } from 'hono/cookie';
|
|
6
8
|
import { deserializeArrayParameter, deserializeObjectParameter, getObjectPropertyNames, isArraySchema, isObjectSchema, resolveSerialization, } from './deserialize-parameter.js';
|
|
9
|
+
import { findQuerystringParameter, getQuerystringJsonSchema, parseQuerystringParameter } from './querystring-parameter.js';
|
|
7
10
|
import { replaceCircularMarkers } from './replace-circular-markers.js';
|
|
8
11
|
/**
|
|
9
12
|
* Prepare a resolved schema for Ajv by replacing the `'[circular]'` markers a recursive schema leaves
|
|
@@ -113,7 +116,7 @@ const mergeParameters = (pathItemParameters, operationParameters) => {
|
|
|
113
116
|
* in isolation so one broken schema never disables the others.
|
|
114
117
|
*/
|
|
115
118
|
const compileSchema = (ajv, schema, label) => {
|
|
116
|
-
if (
|
|
119
|
+
if (schema === null || schema === undefined) {
|
|
117
120
|
return null;
|
|
118
121
|
}
|
|
119
122
|
try {
|
|
@@ -140,6 +143,9 @@ const compileValidators = (operation, pathItemParameters) => {
|
|
|
140
143
|
const queryParameters = buildParameterSchema(parameters, 'query');
|
|
141
144
|
const headerParameters = buildParameterSchema(parameters, 'header');
|
|
142
145
|
const cookieParameters = buildParameterSchema(parameters, 'cookie');
|
|
146
|
+
const querystringParameter = findQuerystringParameter(operation, pathItemParameters);
|
|
147
|
+
const [querystringContentType, querystringMedia] = getFirstMediaType(querystringParameter?.content) ?? [];
|
|
148
|
+
const querystringSchema = querystringMedia?.schema;
|
|
143
149
|
const requestBody = getResolvedRef(operation.requestBody);
|
|
144
150
|
// Build the body schema defensively; resolving a malformed `$ref` should not crash setup.
|
|
145
151
|
let bodySchema = null;
|
|
@@ -151,6 +157,9 @@ const compileValidators = (operation, pathItemParameters) => {
|
|
|
151
157
|
console.error('Error resolving request body schema, skipping body validation:', error);
|
|
152
158
|
}
|
|
153
159
|
return {
|
|
160
|
+
querystringParameter,
|
|
161
|
+
querystringJson: compileSchema(bodyAjv, getQuerystringJsonSchema(querystringParameter), 'querystring JSON properties'),
|
|
162
|
+
querystring: compileSchema(parseMimeType(querystringContentType).essence === 'application/x-www-form-urlencoded' ? parameterAjv : bodyAjv, querystringSchema === undefined ? null : asCompilableSchema(getResolvedRefDeep(querystringSchema)), 'querystring parameter'),
|
|
154
163
|
path: compileSchema(parameterAjv, pathParameters?.schema ?? null, 'path parameter'),
|
|
155
164
|
query: compileSchema(parameterAjv, queryParameters?.schema ?? null, 'query parameter'),
|
|
156
165
|
header: compileSchema(parameterAjv, headerParameters?.schema ?? null, 'header parameter'),
|
|
@@ -312,6 +321,25 @@ export const validateRequest = (operation, pathItemParameters) => {
|
|
|
312
321
|
violations.push(...mapErrors(validator.errors, location));
|
|
313
322
|
}
|
|
314
323
|
}
|
|
324
|
+
if (validators.querystringParameter) {
|
|
325
|
+
try {
|
|
326
|
+
const value = parseQuerystringParameter(c.req.url, validators.querystringParameter);
|
|
327
|
+
if (value === undefined) {
|
|
328
|
+
if (validators.querystringParameter.required) {
|
|
329
|
+
violations.push({ location: 'query', path: '', message: 'Query string is required' });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (validators.querystringJson && !validators.querystringJson(value)) {
|
|
333
|
+
violations.push(...mapErrors(validators.querystringJson.errors, 'query'));
|
|
334
|
+
}
|
|
335
|
+
else if (validators.querystring && !validators.querystring(value)) {
|
|
336
|
+
violations.push(...mapErrors(validators.querystring.errors, 'query'));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
violations.push({ location: 'query', path: '', message: 'Query string could not be decoded' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
315
343
|
// Request body — only `application/json` in this slice
|
|
316
344
|
if (validators.body || validators.bodyRequired) {
|
|
317
345
|
// Read from a clone so the original request stream stays intact for the mock or `x-handler`
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"swagger",
|
|
17
17
|
"cli"
|
|
18
18
|
],
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.17.0",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@faker-js/faker": "10.6.0",
|
|
55
55
|
"@hono/node-server": "^2.1.1",
|
|
56
|
-
"@scalar/helpers": "0.
|
|
57
|
-
"@scalar/json-magic": "0.15.
|
|
56
|
+
"@scalar/helpers": "0.15.0",
|
|
57
|
+
"@scalar/json-magic": "0.15.2",
|
|
58
58
|
"@scalar/openapi-types": "0.9.7",
|
|
59
|
-
"@scalar/openapi-upgrader": "0.
|
|
60
|
-
"@scalar/types": "0.
|
|
61
|
-
"@scalar/workspace-store": "0.
|
|
59
|
+
"@scalar/openapi-upgrader": "0.4.0",
|
|
60
|
+
"@scalar/types": "0.22.1",
|
|
61
|
+
"@scalar/workspace-store": "0.67.0",
|
|
62
62
|
"ajv": "^8.20.0",
|
|
63
63
|
"ajv-formats": "^3.0.1",
|
|
64
64
|
"hono": "^4.13.8",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/node": "^24.1.0",
|
|
71
71
|
"@types/ws": "8.18.1",
|
|
72
|
-
"undici": "7.
|
|
72
|
+
"undici": "7.29.0",
|
|
73
73
|
"vite": "8.1.5"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|