@scalar/mock-server 0.15.0 → 0.16.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 +12 -0
- package/dist/create-mock-server.d.ts.map +1 -1
- package/dist/create-mock-server.js +24 -13
- 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 +2 -2
- 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/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/validate-request.d.ts.map +1 -1
- package/dist/utils/validate-request.js +29 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @scalar/mock-server
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
- [#10186](https://github.com/scalar/scalar/pull/10186): Support additionalOperations with case-sensitive custom HTTP methods, existing operation middleware, and CORS preflight responses.
|
|
12
|
+
|
|
13
|
+
Preserve QUERY in the default CORS method list when adding methods declared by the API description.
|
|
14
|
+
|
|
3
15
|
## 0.15.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -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] }));
|
|
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));
|
|
192
196
|
}
|
|
193
197
|
else {
|
|
194
198
|
handlers.push(async (c) => await mockAnyResponse(c, operation));
|
|
195
199
|
}
|
|
196
|
-
if (query.length === 0) {
|
|
197
|
-
handlers.forEach((handler) => app[method](route, handler));
|
|
198
|
-
return;
|
|
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
|
}
|
|
@@ -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']): 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":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AA4HnC;;;GAGG;AACH,wBAAsB,mBAAmB,
|
|
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;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AA4HnC;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,WAAW,CAAC,eAAe,EACtC,kBAAkB,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,qBAqE9D"}
|
|
@@ -98,7 +98,7 @@ function determineStatusCode(tracking) {
|
|
|
98
98
|
* Mock response using x-handler code.
|
|
99
99
|
* Executes the handler and returns its result as the response.
|
|
100
100
|
*/
|
|
101
|
-
export async function mockHandlerResponse(c, operation) {
|
|
101
|
+
export async function mockHandlerResponse(c, operation, pathItemParameters) {
|
|
102
102
|
// Note: the `onRequest` callback runs as middleware (see `create-mock-server`) so it also fires
|
|
103
103
|
// for requests rejected before reaching this handler.
|
|
104
104
|
// Get x-handler code from operation
|
|
@@ -109,7 +109,7 @@ export async function mockHandlerResponse(c, operation) {
|
|
|
109
109
|
}
|
|
110
110
|
try {
|
|
111
111
|
// Build handler context with tracking
|
|
112
|
-
const { context, tracking } = await buildHandlerContext(c, operation);
|
|
112
|
+
const { context, tracking } = await buildHandlerContext(c, operation, pathItemParameters);
|
|
113
113
|
// Execute handler
|
|
114
114
|
const { result } = await executeHandler(handlerCode, context);
|
|
115
115
|
// Determine status code based on all store operations, prioritizing semantically meaningful ones
|
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
|
+
};
|
|
@@ -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 +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.16.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.14.0",
|
|
57
|
+
"@scalar/json-magic": "0.15.1",
|
|
58
58
|
"@scalar/openapi-types": "0.9.7",
|
|
59
|
-
"@scalar/openapi-upgrader": "0.3.
|
|
60
|
-
"@scalar/types": "0.
|
|
61
|
-
"@scalar/workspace-store": "0.
|
|
59
|
+
"@scalar/openapi-upgrader": "0.3.1",
|
|
60
|
+
"@scalar/types": "0.22.0",
|
|
61
|
+
"@scalar/workspace-store": "0.66.0",
|
|
62
62
|
"ajv": "^8.20.0",
|
|
63
63
|
"ajv-formats": "^3.0.1",
|
|
64
64
|
"hono": "^4.13.8",
|