swaggerizer 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/admin.d.ts +2 -1
- package/dist/admin.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +651 -33
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +654 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +651 -35
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/types.d.ts +30 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/app.css +10 -0
- package/dist/ui/app.js +25 -0
- package/dist/validate.d.ts +33 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +3 -1
- package/dist/chunks/scenario-5a1VnDmg.mjs +0 -345
- package/dist/chunks/scenario-5a1VnDmg.mjs.map +0 -1
- package/dist/chunks/scenario-BeXAfctR.mjs +0 -347
- package/dist/chunks/scenario-BeXAfctR.mjs.map +0 -1
- package/dist/chunks/scenario-CGR-NuEy.mjs +0 -371
- package/dist/chunks/scenario-CGR-NuEy.mjs.map +0 -1
- package/dist/chunks/scenario-DKCQMD5-.js +0 -364
- package/dist/chunks/scenario-DKCQMD5-.js.map +0 -1
- package/dist/chunks/scenario-yqSnVp0L.js +0 -362
- package/dist/chunks/scenario-yqSnVp0L.js.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { H as HTTP_METHODS, o as operationKey, E as EMPTY, G as GENERATED, a as applySelection, b as buildOperationStates, m as matchOperation, p as parseScenarioHeader, S as SCENARIO_HEADER,
|
|
1
|
+
import { H as HTTP_METHODS, o as operationKey, E as EMPTY, G as GENERATED, a as applySelection, b as buildOperationStates, m as matchOperation, p as parseScenarioHeader, S as SCENARIO_HEADER, r as resolveResponse, c as candidatePaths, e as encodeBody, d as relativeToBase } from './chunks/scenario-DXI-TbMN.mjs';
|
|
2
2
|
export { f as applyAliases, g as effectiveExample, v as variantsOf } from './chunks/scenario-DXI-TbMN.mjs';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
8
|
+
import Ajv from 'ajv';
|
|
9
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
10
|
+
import addFormats from 'ajv-formats';
|
|
8
11
|
import { applyOverlays, unmatchedKeys, lookupByOperation, extractParams, createHandlerContext, isHandlerResult } from './config.mjs';
|
|
9
12
|
export { defineConfig, result } from './config.mjs';
|
|
10
13
|
import os from 'node:os';
|
|
@@ -106,6 +109,23 @@ function sendJson(res, status, payload) {
|
|
|
106
109
|
function blank(value) {
|
|
107
110
|
return value === undefined || value === null || value === "";
|
|
108
111
|
}
|
|
112
|
+
/** MSW reports request summaries to the live log; keep only the fields the page renders. */
|
|
113
|
+
function toValidation(raw) {
|
|
114
|
+
if (!raw || typeof raw !== "object")
|
|
115
|
+
return undefined;
|
|
116
|
+
const text = (value, max = 200) => typeof value === "string" && value !== "" ? value.slice(0, max) : undefined;
|
|
117
|
+
const request = (Array.isArray(raw.request) ? raw.request : [])
|
|
118
|
+
.slice(0, 20)
|
|
119
|
+
.map((issue) => ({
|
|
120
|
+
location: text(issue?.location, 120) ?? "request",
|
|
121
|
+
message: text(issue?.message) ?? "is invalid",
|
|
122
|
+
...(text(issue?.keyword, 40) ? { keyword: text(issue?.keyword, 40) } : {})
|
|
123
|
+
}));
|
|
124
|
+
const skipped = text(raw.skipped, 60);
|
|
125
|
+
if (request.length === 0 && !skipped)
|
|
126
|
+
return undefined;
|
|
127
|
+
return { request, ...(skipped ? { skipped } : {}) };
|
|
128
|
+
}
|
|
109
129
|
/**
|
|
110
130
|
* Turns a flat record (query string, form post or JSON) into a selection. Accepts the ramlizer
|
|
111
131
|
* names (`nextResponseCode`, `nextExampleName`) and flat custom fields (`status`, `body`, `contentType`).
|
|
@@ -200,6 +220,10 @@ function registerAdmin(ctx) {
|
|
|
200
220
|
sendJson(res, 200, { changes: ctx.getState().changes });
|
|
201
221
|
return true;
|
|
202
222
|
}
|
|
223
|
+
if (method === "GET" && path === "/api/validation") {
|
|
224
|
+
sendJson(res, 200, ctx.getValidation());
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
203
227
|
if (path === "/api/log") {
|
|
204
228
|
if (method === "GET") {
|
|
205
229
|
const since = Number(query.get("since") || 0);
|
|
@@ -209,16 +233,20 @@ function registerAdmin(ctx) {
|
|
|
209
233
|
if (method === "POST") {
|
|
210
234
|
// Lets the MSW runtime report requests it answered in the browser or in tests.
|
|
211
235
|
const items = Array.isArray(req.body) ? req.body : [req.body];
|
|
212
|
-
const entries = items.slice(0, 50).map(item =>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
const entries = items.slice(0, 50).map(item => {
|
|
237
|
+
const validation = toValidation(item?.validation);
|
|
238
|
+
return ctx.recordRequest({
|
|
239
|
+
method: String(item?.method || "GET").toUpperCase(),
|
|
240
|
+
path: String(item?.path || ""),
|
|
241
|
+
...(item?.route ? { route: String(item.route) } : {}),
|
|
242
|
+
status: Number(item?.status) || 0,
|
|
243
|
+
example: String(item?.example || ""),
|
|
244
|
+
...(item?.specId ? { specId: String(item.specId) } : {}),
|
|
245
|
+
...(item?.ms !== undefined ? { ms: Number(item.ms) } : {}),
|
|
246
|
+
...(validation ? { validation } : {}),
|
|
247
|
+
source: "msw"
|
|
248
|
+
});
|
|
249
|
+
});
|
|
222
250
|
sendJson(res, 200, { recorded: entries.length });
|
|
223
251
|
return true;
|
|
224
252
|
}
|
|
@@ -1230,6 +1258,494 @@ async function resolveSpecInputs(inputs) {
|
|
|
1230
1258
|
return [...new Set(out)];
|
|
1231
1259
|
}
|
|
1232
1260
|
|
|
1261
|
+
/*
|
|
1262
|
+
* Request validation: checks a matched request against the operation's declared parameters and
|
|
1263
|
+
* request body, so a mock cannot answer `200` to a request the contract forbids.
|
|
1264
|
+
*
|
|
1265
|
+
* Node only. Ajv must never reach the browser entry points (`msw`, `debug-menu`, `config`), which is
|
|
1266
|
+
* why the shared types live in `./types` and this module is not re-exported from them.
|
|
1267
|
+
*
|
|
1268
|
+
* Specs are bundled but not dereferenced: every operation schema is compiled on its own, with its
|
|
1269
|
+
* internal `#/...` refs pointed at the registered document. That keeps recursive schemas valid.
|
|
1270
|
+
*/
|
|
1271
|
+
const VALIDATION_HEADER = "x-swaggerizer-validation";
|
|
1272
|
+
const MAX_ISSUES = 20;
|
|
1273
|
+
const IGNORED_HEADER_PARAMS = new Set(["accept", "content-type", "authorization"]);
|
|
1274
|
+
const PARAM_LOCATIONS = new Set(["path", "query", "header", "cookie"]);
|
|
1275
|
+
const SKIP = Symbol("swaggerizer-skip");
|
|
1276
|
+
/* ------------------------------------------------------------------ schema preparation */
|
|
1277
|
+
/** OpenAPI 3.0 spells nullability as `nullable: true`; JSON Schema wants a `null` type. */
|
|
1278
|
+
function withoutNullable(node) {
|
|
1279
|
+
if (Array.isArray(node))
|
|
1280
|
+
return node.map(withoutNullable);
|
|
1281
|
+
if (!node || typeof node !== "object")
|
|
1282
|
+
return node;
|
|
1283
|
+
const out = {};
|
|
1284
|
+
for (const [key, value] of Object.entries(node))
|
|
1285
|
+
out[key] = withoutNullable(value);
|
|
1286
|
+
if (out.nullable !== true)
|
|
1287
|
+
return out;
|
|
1288
|
+
delete out.nullable;
|
|
1289
|
+
if (typeof out.type === "string")
|
|
1290
|
+
out.type = [out.type, "null"];
|
|
1291
|
+
else if (Array.isArray(out.type)) {
|
|
1292
|
+
if (!out.type.includes("null"))
|
|
1293
|
+
out.type = [...out.type, "null"];
|
|
1294
|
+
}
|
|
1295
|
+
else if (out.$ref || out.allOf || out.oneOf || out.anyOf || out.not) {
|
|
1296
|
+
return { anyOf: [out, { type: "null" }] };
|
|
1297
|
+
}
|
|
1298
|
+
return out;
|
|
1299
|
+
}
|
|
1300
|
+
/** Points a schema's internal refs at the registered document so it can be compiled standalone. */
|
|
1301
|
+
function withBaseRefs(value, base) {
|
|
1302
|
+
if (Array.isArray(value))
|
|
1303
|
+
return value.map(entry => withBaseRefs(entry, base));
|
|
1304
|
+
if (!value || typeof value !== "object")
|
|
1305
|
+
return value;
|
|
1306
|
+
const out = {};
|
|
1307
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1308
|
+
out[key] = key === "$ref" && typeof entry === "string" && entry.startsWith("#") ? `${base}${entry}` : withBaseRefs(entry, base);
|
|
1309
|
+
}
|
|
1310
|
+
return out;
|
|
1311
|
+
}
|
|
1312
|
+
function createAjv(specVersion) {
|
|
1313
|
+
// OAS 3.1 is JSON Schema 2020-12, where `exclusiveMinimum` is a number; draft-07 would reject it.
|
|
1314
|
+
const Ctor = typeof specVersion === "string" && /^3\.[1-9]/.test(specVersion) ? Ajv2020 : Ajv;
|
|
1315
|
+
const ajv = new Ctor({
|
|
1316
|
+
strict: false,
|
|
1317
|
+
allErrors: true,
|
|
1318
|
+
coerceTypes: false,
|
|
1319
|
+
useDefaults: false,
|
|
1320
|
+
allowUnionTypes: true,
|
|
1321
|
+
validateFormats: true
|
|
1322
|
+
});
|
|
1323
|
+
addFormats(ajv);
|
|
1324
|
+
return ajv;
|
|
1325
|
+
}
|
|
1326
|
+
function typeOf(schema) {
|
|
1327
|
+
const type = schema?.type;
|
|
1328
|
+
if (Array.isArray(type))
|
|
1329
|
+
return type.find((entry) => entry !== "null") ?? type[0];
|
|
1330
|
+
if (typeof type === "string")
|
|
1331
|
+
return type;
|
|
1332
|
+
if (schema?.properties || schema?.additionalProperties)
|
|
1333
|
+
return "object";
|
|
1334
|
+
if (schema?.items)
|
|
1335
|
+
return "array";
|
|
1336
|
+
return undefined;
|
|
1337
|
+
}
|
|
1338
|
+
function headerValue(headers, name) {
|
|
1339
|
+
const value = headers[name.toLowerCase()];
|
|
1340
|
+
if (Array.isArray(value))
|
|
1341
|
+
return value.join(", ");
|
|
1342
|
+
return value === undefined ? undefined : String(value);
|
|
1343
|
+
}
|
|
1344
|
+
/* ------------------------------------------------------------------ parameters */
|
|
1345
|
+
/** Delimiter of a non-exploded array: `form` is comma-separated, the others say so in their name. */
|
|
1346
|
+
function arrayDelimiter(style) {
|
|
1347
|
+
if (style === "spaceDelimited")
|
|
1348
|
+
return " ";
|
|
1349
|
+
if (style === "pipeDelimited")
|
|
1350
|
+
return "|";
|
|
1351
|
+
return ",";
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Exploded `form` objects arrive as their own top-level keys (`?a=1&b=2`) and `deepObject` as
|
|
1355
|
+
* `name[key]=…`. Neither is rebuilt here, so such parameters are reported as skipped, not failed.
|
|
1356
|
+
*/
|
|
1357
|
+
function isSpreadObject(param, type) {
|
|
1358
|
+
return (!param.json &&
|
|
1359
|
+
type === "object" &&
|
|
1360
|
+
(param.in === "query" || param.in === "cookie") &&
|
|
1361
|
+
((param.style === "form" && param.explode) || param.style === "deepObject"));
|
|
1362
|
+
}
|
|
1363
|
+
function hasDeepObjectKeys(query, name) {
|
|
1364
|
+
for (const key of query.keys())
|
|
1365
|
+
if (key.startsWith(`${name}[`))
|
|
1366
|
+
return true;
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
/** The value a parameter arrived as: repeated query keys for arrays, the last cookie for scalars. */
|
|
1370
|
+
function rawParam(param, input, type) {
|
|
1371
|
+
if (param.in === "path")
|
|
1372
|
+
return input.params[param.name];
|
|
1373
|
+
if (param.in === "query") {
|
|
1374
|
+
if (type === "array" && !param.json)
|
|
1375
|
+
return input.query.getAll(param.name);
|
|
1376
|
+
return input.query.get(param.name) ?? undefined;
|
|
1377
|
+
}
|
|
1378
|
+
if (param.in === "header")
|
|
1379
|
+
return headerValue(input.headers, param.name);
|
|
1380
|
+
const cookie = headerValue(input.headers, "cookie");
|
|
1381
|
+
if (!cookie)
|
|
1382
|
+
return undefined;
|
|
1383
|
+
const found = [];
|
|
1384
|
+
for (const part of cookie.split(";")) {
|
|
1385
|
+
const at = part.indexOf("=");
|
|
1386
|
+
if (at < 0)
|
|
1387
|
+
continue;
|
|
1388
|
+
if (part.slice(0, at).trim() === param.name)
|
|
1389
|
+
found.push(part.slice(at + 1).trim());
|
|
1390
|
+
}
|
|
1391
|
+
if (found.length === 0)
|
|
1392
|
+
return undefined;
|
|
1393
|
+
return type === "array" ? found : found[found.length - 1];
|
|
1394
|
+
}
|
|
1395
|
+
function coerceScalar(doc, value, schemaIn) {
|
|
1396
|
+
const { node: schema } = deref(doc, schemaIn);
|
|
1397
|
+
const type = typeOf(schema);
|
|
1398
|
+
if (type === "integer" || type === "number") {
|
|
1399
|
+
return /^[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/.test(value) ? Number(value) : value;
|
|
1400
|
+
}
|
|
1401
|
+
if (type === "boolean")
|
|
1402
|
+
return value === "true" ? true : value === "false" ? false : value;
|
|
1403
|
+
return value;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* HTTP hands us strings; the spec talks in types. Validation runs against a coerced copy, so
|
|
1407
|
+
* handlers keep receiving exactly what they always did.
|
|
1408
|
+
*/
|
|
1409
|
+
function coerceParam(doc, param, raw, summary) {
|
|
1410
|
+
const { node: schema } = deref(doc, param.schema);
|
|
1411
|
+
const type = typeOf(schema);
|
|
1412
|
+
if (type === "array") {
|
|
1413
|
+
// Repeated keys (`ids=1&ids=2`) are already separate; a non-exploded value (`ids=1,2`) is split.
|
|
1414
|
+
const split = (value, delimiter) => value.split(delimiter).map(part => part.trim()).filter(part => part !== "");
|
|
1415
|
+
const values = Array.isArray(raw)
|
|
1416
|
+
? param.explode
|
|
1417
|
+
? raw
|
|
1418
|
+
: raw.flatMap(value => split(value, arrayDelimiter(param.style)))
|
|
1419
|
+
: split(String(raw), ",");
|
|
1420
|
+
return values.map(value => coerceScalar(doc, value, schema?.items));
|
|
1421
|
+
}
|
|
1422
|
+
if (type === "object") {
|
|
1423
|
+
if (Array.isArray(raw))
|
|
1424
|
+
return raw;
|
|
1425
|
+
if (isSpreadObject(param, type)) {
|
|
1426
|
+
summary.skipped ??= "object-style";
|
|
1427
|
+
return SKIP;
|
|
1428
|
+
}
|
|
1429
|
+
const out = {};
|
|
1430
|
+
for (const pair of String(raw).split(",")) {
|
|
1431
|
+
if (pair === "")
|
|
1432
|
+
continue;
|
|
1433
|
+
const at = pair.indexOf(":");
|
|
1434
|
+
if (at < 0) {
|
|
1435
|
+
out[pair.trim()] = true;
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
const key = pair.slice(0, at).trim();
|
|
1439
|
+
out[key] = coerceScalar(doc, pair.slice(at + 1).trim(), schema?.properties?.[key] ?? schema?.additionalProperties);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return out;
|
|
1443
|
+
}
|
|
1444
|
+
return Array.isArray(raw) ? raw[raw.length - 1] : coerceScalar(doc, String(raw), schema);
|
|
1445
|
+
}
|
|
1446
|
+
function coerceForm(doc, body, schemaIn, summary) {
|
|
1447
|
+
const { node: schema } = deref(doc, schemaIn);
|
|
1448
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
1449
|
+
return body;
|
|
1450
|
+
const properties = schema?.properties || {};
|
|
1451
|
+
const out = {};
|
|
1452
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1453
|
+
const property = properties[key];
|
|
1454
|
+
const propertyType = typeOf(deref(doc, property).node);
|
|
1455
|
+
if (propertyType === "object") {
|
|
1456
|
+
// Deep form bodies need a parser this tool does not ship; report the gap instead of a false pass.
|
|
1457
|
+
summary.skipped ??= "form-nesting";
|
|
1458
|
+
out[key] = value;
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (propertyType === "array" && typeof value === "string") {
|
|
1462
|
+
out[key] = value.split(",").map(entry => coerceScalar(doc, entry.trim(), deref(doc, property).node?.items));
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
out[key] = typeof value === "string" ? coerceScalar(doc, value, property) : value;
|
|
1466
|
+
}
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
/* ------------------------------------------------------------------ bodies */
|
|
1470
|
+
function normaliseContentType(value) {
|
|
1471
|
+
if (!value)
|
|
1472
|
+
return undefined;
|
|
1473
|
+
const type = String(value).split(";")[0].trim().toLowerCase();
|
|
1474
|
+
if (!type)
|
|
1475
|
+
return undefined;
|
|
1476
|
+
return type === "*/*" || type === "*" || type === "application/*" ? "application/json" : type;
|
|
1477
|
+
}
|
|
1478
|
+
/** The declared media type this request should be checked against, if any. */
|
|
1479
|
+
function matchMediaType(declared, actual) {
|
|
1480
|
+
if (declared.length === 0)
|
|
1481
|
+
return undefined;
|
|
1482
|
+
const types = declared.map(key => ({ key, type: key.split(";")[0].trim().toLowerCase() }));
|
|
1483
|
+
const wanted = actual ?? "application/json";
|
|
1484
|
+
const exact = types.find(entry => entry.type === wanted);
|
|
1485
|
+
if (exact)
|
|
1486
|
+
return exact.key;
|
|
1487
|
+
if (wanted.endsWith("+json")) {
|
|
1488
|
+
const json = types.find(entry => entry.type === "application/json");
|
|
1489
|
+
if (json)
|
|
1490
|
+
return json.key;
|
|
1491
|
+
}
|
|
1492
|
+
const wildcard = types.find(entry => entry.type === "*/*" || entry.type === "application/*");
|
|
1493
|
+
return wildcard?.key;
|
|
1494
|
+
}
|
|
1495
|
+
const FORM_TYPE = "application/x-www-form-urlencoded";
|
|
1496
|
+
/**
|
|
1497
|
+
* Bodies the server can parse: JSON (`+json` included) and urlencoded forms. No `Content-Type` means
|
|
1498
|
+
* JSON, as it always has. Multipart, octet-stream, text… are skipped rather than failed to parse.
|
|
1499
|
+
*/
|
|
1500
|
+
function readableMediaType(actual) {
|
|
1501
|
+
if (!actual)
|
|
1502
|
+
return true;
|
|
1503
|
+
return actual === "application/json" || actual.endsWith("+json") || actual === FORM_TYPE;
|
|
1504
|
+
}
|
|
1505
|
+
/** True when the operation (or its path item) declares any parameter or a request body. */
|
|
1506
|
+
function hasRequestSchema(doc, op) {
|
|
1507
|
+
const item = doc?.paths?.[op.route];
|
|
1508
|
+
const node = item?.[op.method];
|
|
1509
|
+
return Boolean((Array.isArray(item?.parameters) && item.parameters.length > 0) ||
|
|
1510
|
+
(Array.isArray(node?.parameters) && node.parameters.length > 0) ||
|
|
1511
|
+
node?.requestBody);
|
|
1512
|
+
}
|
|
1513
|
+
/* ------------------------------------------------------------------ issues */
|
|
1514
|
+
function pointerPath(pointer) {
|
|
1515
|
+
if (typeof pointer !== "string" || pointer === "")
|
|
1516
|
+
return "";
|
|
1517
|
+
return pointer.split("/").filter(Boolean).map(decodePointerSegment).join(".");
|
|
1518
|
+
}
|
|
1519
|
+
function issueFromError(error, prefix) {
|
|
1520
|
+
const path = pointerPath(error?.instancePath);
|
|
1521
|
+
const missing = error?.keyword === "required" ? error?.params?.missingProperty : undefined;
|
|
1522
|
+
const location = [prefix, path, missing].filter(Boolean).join(".");
|
|
1523
|
+
return {
|
|
1524
|
+
location,
|
|
1525
|
+
message: String(error?.message ?? "is invalid"),
|
|
1526
|
+
...(error?.keyword ? { keyword: String(error.keyword) } : {})
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function add(summary, issue) {
|
|
1530
|
+
if (summary.request.length < MAX_ISSUES)
|
|
1531
|
+
summary.request.push(issue);
|
|
1532
|
+
}
|
|
1533
|
+
function runCheck(validate, value) {
|
|
1534
|
+
if (!validate)
|
|
1535
|
+
return [];
|
|
1536
|
+
return validate(value) ? [] : validate.errors ?? [];
|
|
1537
|
+
}
|
|
1538
|
+
/* ------------------------------------------------------------------ matching helpers */
|
|
1539
|
+
/** `X-Swaggerizer-Validation: off | warn | enforce`; unknown values fall back to the configured mode. */
|
|
1540
|
+
function parseValidationMode(value) {
|
|
1541
|
+
if (typeof value !== "string")
|
|
1542
|
+
return undefined;
|
|
1543
|
+
const mode = value.trim().toLowerCase();
|
|
1544
|
+
return mode === "off" || mode === "warn" || mode === "enforce" ? mode : undefined;
|
|
1545
|
+
}
|
|
1546
|
+
/** True when one of `validateIgnore`'s operationIds or `"METHOD /route"` entries covers `op`. */
|
|
1547
|
+
function matchesIgnore(ignore, op) {
|
|
1548
|
+
if (!ignore || ignore.length === 0)
|
|
1549
|
+
return false;
|
|
1550
|
+
return ignore.some(entry => {
|
|
1551
|
+
const value = String(entry).trim();
|
|
1552
|
+
return value === op.operationId || value === `${op.method.toUpperCase()} ${op.route}` || value === op.key;
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
/** `validateIgnore` entries that match no operation, reported as a spec warning. */
|
|
1556
|
+
function unmatchedIgnore(ignore, operations) {
|
|
1557
|
+
if (!ignore || ignore.length === 0)
|
|
1558
|
+
return [];
|
|
1559
|
+
return ignore.filter(entry => !operations.some(op => entry === op.operationId ||
|
|
1560
|
+
entry === `${op.method.toUpperCase()} ${op.route}` ||
|
|
1561
|
+
entry === op.key));
|
|
1562
|
+
}
|
|
1563
|
+
/** "query.limit: must be integer" — used for log tooltips and CLI output. */
|
|
1564
|
+
function formatIssues(issues) {
|
|
1565
|
+
return issues.map(issue => `${issue.location}: ${issue.message}`).join("; ");
|
|
1566
|
+
}
|
|
1567
|
+
/* ------------------------------------------------------------------ the validator */
|
|
1568
|
+
function createValidator(doc, specId, specVersion) {
|
|
1569
|
+
const base = `urn:swaggerizer:${encodeURIComponent(specId)}`;
|
|
1570
|
+
const ajv = createAjv(specVersion);
|
|
1571
|
+
try {
|
|
1572
|
+
ajv.addSchema(withoutNullable(structuredClone(doc)), base);
|
|
1573
|
+
}
|
|
1574
|
+
catch {
|
|
1575
|
+
// A document Ajv refuses to register simply leaves every check skipped, never the mock broken.
|
|
1576
|
+
}
|
|
1577
|
+
const plans = new Map();
|
|
1578
|
+
const bodyChecks = new Map();
|
|
1579
|
+
function compile(schema) {
|
|
1580
|
+
try {
|
|
1581
|
+
// Inline operation schemas get the same `nullable` rewrite as the registered document.
|
|
1582
|
+
return ajv.compile(withBaseRefs(withoutNullable(schema), base));
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
return undefined;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function buildPlan(op) {
|
|
1589
|
+
const item = doc?.paths?.[op.route];
|
|
1590
|
+
const node = item?.[op.method];
|
|
1591
|
+
const plan = { params: [] };
|
|
1592
|
+
// An operation-level parameter replaces a path-level one with the same name and location.
|
|
1593
|
+
const declared = new Map();
|
|
1594
|
+
for (const entry of [
|
|
1595
|
+
...(Array.isArray(item?.parameters) ? item.parameters : []),
|
|
1596
|
+
...(Array.isArray(node?.parameters) ? node.parameters : [])
|
|
1597
|
+
]) {
|
|
1598
|
+
const { node: param } = deref(doc, entry);
|
|
1599
|
+
if (!param || typeof param !== "object" || !param.name || typeof param.in !== "string")
|
|
1600
|
+
continue;
|
|
1601
|
+
const name = param.in === "header" ? String(param.name).toLowerCase() : String(param.name);
|
|
1602
|
+
declared.set(`${param.in}\u0000${name}`, param);
|
|
1603
|
+
}
|
|
1604
|
+
for (const param of declared.values()) {
|
|
1605
|
+
if (!PARAM_LOCATIONS.has(param.in))
|
|
1606
|
+
continue;
|
|
1607
|
+
const where = param.in;
|
|
1608
|
+
if (where === "header" && IGNORED_HEADER_PARAMS.has(String(param.name).toLowerCase()))
|
|
1609
|
+
continue;
|
|
1610
|
+
let schema = param.schema;
|
|
1611
|
+
let json = false;
|
|
1612
|
+
if (!schema && param.content && typeof param.content === "object") {
|
|
1613
|
+
// Only a JSON `content` parameter can be read; any other media type is left unchecked.
|
|
1614
|
+
const key = Object.keys(param.content).find(entry => /^application\/(.+\+)?json\b/i.test(entry.trim()));
|
|
1615
|
+
if (key) {
|
|
1616
|
+
schema = param.content[key]?.schema;
|
|
1617
|
+
json = true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (!schema)
|
|
1621
|
+
continue;
|
|
1622
|
+
const style = typeof param.style === "string" ? param.style : where === "query" || where === "cookie" ? "form" : "simple";
|
|
1623
|
+
const compiled = compile(schema);
|
|
1624
|
+
plan.params.push({
|
|
1625
|
+
name: String(param.name),
|
|
1626
|
+
in: where,
|
|
1627
|
+
required: where === "path" || param.required === true,
|
|
1628
|
+
style,
|
|
1629
|
+
explode: typeof param.explode === "boolean" ? param.explode : style === "form",
|
|
1630
|
+
schema,
|
|
1631
|
+
json,
|
|
1632
|
+
...(compiled ? { validate: compiled } : { problem: "invalid-schema" })
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
const requestBody = node?.requestBody ? deref(doc, node.requestBody).node : undefined;
|
|
1636
|
+
if (requestBody && typeof requestBody === "object" && requestBody.content && typeof requestBody.content === "object") {
|
|
1637
|
+
plan.body = { required: requestBody.required === true, content: requestBody.content };
|
|
1638
|
+
}
|
|
1639
|
+
return plan;
|
|
1640
|
+
}
|
|
1641
|
+
function planFor(op) {
|
|
1642
|
+
let plan = plans.get(op.key);
|
|
1643
|
+
if (!plan) {
|
|
1644
|
+
plan = buildPlan(op);
|
|
1645
|
+
plans.set(op.key, plan);
|
|
1646
|
+
}
|
|
1647
|
+
return plan;
|
|
1648
|
+
}
|
|
1649
|
+
function bodyCheck(op, mediaKey, schema) {
|
|
1650
|
+
const key = `${op.key}\u0000${mediaKey}`;
|
|
1651
|
+
if (bodyChecks.has(key))
|
|
1652
|
+
return bodyChecks.get(key);
|
|
1653
|
+
const compiled = compile(schema);
|
|
1654
|
+
bodyChecks.set(key, compiled);
|
|
1655
|
+
return compiled;
|
|
1656
|
+
}
|
|
1657
|
+
function validateRequest(op, input) {
|
|
1658
|
+
const plan = planFor(op);
|
|
1659
|
+
const summary = { request: [] };
|
|
1660
|
+
for (const param of plan.params) {
|
|
1661
|
+
const { node: schema } = deref(doc, param.schema);
|
|
1662
|
+
const type = typeOf(schema);
|
|
1663
|
+
if (isSpreadObject(param, type)) {
|
|
1664
|
+
// `deepObject` keys can at least be seen; exploded form objects cannot be told from other keys.
|
|
1665
|
+
const deep = param.style === "deepObject" && param.in === "query";
|
|
1666
|
+
const present = deep && hasDeepObjectKeys(input.query, param.name);
|
|
1667
|
+
if (deep && !present && param.required) {
|
|
1668
|
+
add(summary, {
|
|
1669
|
+
location: `${param.in}.${param.name}`,
|
|
1670
|
+
message: `must have required parameter '${param.name}'`,
|
|
1671
|
+
keyword: "required"
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
else if (present || param.required) {
|
|
1675
|
+
summary.skipped ??= "object-style";
|
|
1676
|
+
}
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
const raw = rawParam(param, input, type);
|
|
1680
|
+
const missing = raw === undefined || (Array.isArray(raw) ? raw.length === 0 : raw === "");
|
|
1681
|
+
if (missing) {
|
|
1682
|
+
if (param.required) {
|
|
1683
|
+
add(summary, {
|
|
1684
|
+
location: `${param.in}.${param.name}`,
|
|
1685
|
+
message: `must have required parameter '${param.name}'`,
|
|
1686
|
+
keyword: "required"
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
if (param.problem) {
|
|
1692
|
+
summary.skipped ??= param.problem;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
let value;
|
|
1696
|
+
if (param.json) {
|
|
1697
|
+
const text = Array.isArray(raw) ? raw[raw.length - 1] : String(raw);
|
|
1698
|
+
try {
|
|
1699
|
+
value = JSON.parse(text);
|
|
1700
|
+
}
|
|
1701
|
+
catch {
|
|
1702
|
+
add(summary, { location: `${param.in}.${param.name}`, message: "must be valid JSON", keyword: "parse" });
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
value = coerceParam(doc, param, raw, summary);
|
|
1708
|
+
}
|
|
1709
|
+
if (value === SKIP)
|
|
1710
|
+
continue;
|
|
1711
|
+
for (const error of runCheck(param.validate, value)) {
|
|
1712
|
+
add(summary, issueFromError(error, `${param.in}.${param.name}`));
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (plan.body) {
|
|
1716
|
+
const actual = normaliseContentType(input.contentType);
|
|
1717
|
+
const mediaKey = matchMediaType(Object.keys(plan.body.content), actual);
|
|
1718
|
+
if (!mediaKey || !readableMediaType(actual)) {
|
|
1719
|
+
summary.skipped ??= "unsupported-media-type";
|
|
1720
|
+
}
|
|
1721
|
+
else if (input.bodyError) {
|
|
1722
|
+
add(summary, { location: "body", message: input.bodyError, keyword: "parse" });
|
|
1723
|
+
}
|
|
1724
|
+
else if (input.body === undefined) {
|
|
1725
|
+
if (plan.body.required) {
|
|
1726
|
+
add(summary, { location: "body", message: "must have a request body", keyword: "required" });
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
const schema = plan.body.content[mediaKey]?.schema;
|
|
1731
|
+
const check = schema ? bodyCheck(op, mediaKey, schema) : undefined;
|
|
1732
|
+
if (schema && !check) {
|
|
1733
|
+
summary.skipped ??= "invalid-schema";
|
|
1734
|
+
}
|
|
1735
|
+
else if (check) {
|
|
1736
|
+
const form = (actual ?? mediaKey.split(";")[0].trim().toLowerCase()) === FORM_TYPE;
|
|
1737
|
+
const value = form ? coerceForm(doc, input.body, schema, summary) : input.body;
|
|
1738
|
+
for (const error of runCheck(check, value)) {
|
|
1739
|
+
add(summary, issueFromError(error, "body"));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return summary;
|
|
1745
|
+
}
|
|
1746
|
+
return { validateRequest, hasRequestSchema: op => hasRequestSchema(doc, op) };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1233
1749
|
/* The Swaggerizer mock server: serves every loaded spec and exposes the admin API + page. */
|
|
1234
1750
|
const REQUEST_LOG_LIMIT = 300;
|
|
1235
1751
|
const MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
@@ -1321,6 +1837,41 @@ function createSwaggerizer(options = {}) {
|
|
|
1321
1837
|
/* ------------------------------------------------------------------ state */
|
|
1322
1838
|
const ordered = () => [...specs.values()];
|
|
1323
1839
|
const states = () => buildOperationStates(ordered(), scenarios, { defaultDelay: options.delay });
|
|
1840
|
+
/* ------------------------------------------------------------------ request validation */
|
|
1841
|
+
const validators = new Map();
|
|
1842
|
+
function validatorFor(spec) {
|
|
1843
|
+
let validator = validators.get(spec.id);
|
|
1844
|
+
if (!validator) {
|
|
1845
|
+
validator = createValidator(spec.doc, spec.id, spec.specVersion);
|
|
1846
|
+
validators.set(spec.id, validator);
|
|
1847
|
+
}
|
|
1848
|
+
return validator;
|
|
1849
|
+
}
|
|
1850
|
+
/** Header override, then `validateIgnore`, then the spec's mode, then the server's. */
|
|
1851
|
+
function validationModeFor(op, header) {
|
|
1852
|
+
const fromHeader = parseValidationMode(Array.isArray(header) ? header[0] : header);
|
|
1853
|
+
if (fromHeader)
|
|
1854
|
+
return fromHeader;
|
|
1855
|
+
const cfg = configFor(op.specId);
|
|
1856
|
+
if (matchesIgnore(cfg.validateIgnore, op))
|
|
1857
|
+
return "off";
|
|
1858
|
+
return cfg.validate ?? options.specValidation?.[op.specId] ?? options.validate ?? "off";
|
|
1859
|
+
}
|
|
1860
|
+
function validationInfo(operations = states()) {
|
|
1861
|
+
const perSpec = { ...(options.specValidation || {}) };
|
|
1862
|
+
for (const [id, cfg] of Object.entries(specConfigs))
|
|
1863
|
+
if (cfg.validate)
|
|
1864
|
+
perSpec[id] = cfg.validate;
|
|
1865
|
+
return {
|
|
1866
|
+
mode: options.validate ?? "off",
|
|
1867
|
+
perSpec,
|
|
1868
|
+
operations: operations.map(op => {
|
|
1869
|
+
const spec = specs.get(op.specId);
|
|
1870
|
+
// Read straight from the document: reporting this must not build an Ajv instance while validation is off.
|
|
1871
|
+
return { specId: op.specId, key: op.key, hasRequestSchema: spec ? hasRequestSchema(spec.doc, op) : false };
|
|
1872
|
+
})
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1324
1875
|
function summarise(spec) {
|
|
1325
1876
|
return {
|
|
1326
1877
|
id: spec.id,
|
|
@@ -1345,14 +1896,16 @@ function createSwaggerizer(options = {}) {
|
|
|
1345
1896
|
...snapshotReports,
|
|
1346
1897
|
...[...previousReports.values()].filter(r => r.changes.length > 0)
|
|
1347
1898
|
];
|
|
1899
|
+
const operations = states();
|
|
1348
1900
|
return {
|
|
1349
1901
|
endpoint,
|
|
1350
1902
|
...(options.delay ? { defaultDelay: options.delay } : {}),
|
|
1351
1903
|
...(options.fallback ? { fallback: options.fallback } : {}),
|
|
1352
1904
|
specs: ordered().map(summarise),
|
|
1353
|
-
operations
|
|
1905
|
+
operations,
|
|
1354
1906
|
lastReload,
|
|
1355
|
-
changes
|
|
1907
|
+
changes,
|
|
1908
|
+
validation: validationInfo(operations)
|
|
1356
1909
|
};
|
|
1357
1910
|
}
|
|
1358
1911
|
function servingOperation(method, route) {
|
|
@@ -1487,6 +2040,9 @@ function createSwaggerizer(options = {}) {
|
|
|
1487
2040
|
for (const key of unmatchedKeys(cfg.handlers, analysed.operations)) {
|
|
1488
2041
|
analysed.warnings.push(`Custom handler '${key}' matches no operation`);
|
|
1489
2042
|
}
|
|
2043
|
+
for (const key of unmatchedIgnore(cfg.validateIgnore, analysed.operations)) {
|
|
2044
|
+
analysed.warnings.push(`validateIgnore entry '${key}' matches no operation`);
|
|
2045
|
+
}
|
|
1490
2046
|
const enabled = before ? before.enabled : !disabled.has(id);
|
|
1491
2047
|
next.set(id, { ...analysed, enabled, loadedAt: new Date().toISOString() });
|
|
1492
2048
|
if (before && !before.error) {
|
|
@@ -1518,6 +2074,8 @@ function createSwaggerizer(options = {}) {
|
|
|
1518
2074
|
}
|
|
1519
2075
|
}
|
|
1520
2076
|
specs.clear();
|
|
2077
|
+
// Compiled validators hold the old documents; rebuild them lazily against the new ones.
|
|
2078
|
+
validators.clear();
|
|
1521
2079
|
for (const [id, spec] of next)
|
|
1522
2080
|
specs.set(id, spec);
|
|
1523
2081
|
for (const id of previousReports.keys())
|
|
@@ -1592,18 +2150,18 @@ function createSwaggerizer(options = {}) {
|
|
|
1592
2150
|
return reload();
|
|
1593
2151
|
}
|
|
1594
2152
|
/* ------------------------------------------------------------------ mock serving */
|
|
1595
|
-
async function proxy(req, res, target, relativePath, search) {
|
|
2153
|
+
async function proxy(req, res, target, relativePath, search, bufferedBody) {
|
|
1596
2154
|
const upstream = target.replace(/\/+$/, "") + relativePath + search;
|
|
1597
2155
|
const headers = {};
|
|
1598
2156
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
1599
2157
|
if (value === undefined)
|
|
1600
2158
|
continue;
|
|
1601
|
-
if (["host", "connection", "content-length", "accept-encoding", SCENARIO_HEADER].includes(name))
|
|
2159
|
+
if (["host", "connection", "content-length", "accept-encoding", SCENARIO_HEADER, VALIDATION_HEADER].includes(name))
|
|
1602
2160
|
continue;
|
|
1603
2161
|
headers[name] = Array.isArray(value) ? value.join(", ") : value;
|
|
1604
2162
|
}
|
|
1605
2163
|
const method = (req.method || "GET").toUpperCase();
|
|
1606
|
-
const body = method === "GET" || method === "HEAD" ? undefined : await readBody(req);
|
|
2164
|
+
const body = method === "GET" || method === "HEAD" ? undefined : bufferedBody ?? (await readBody(req));
|
|
1607
2165
|
const response = await fetch(upstream, { method, headers, body: body && body.length ? new Uint8Array(body) : undefined, redirect: "manual" });
|
|
1608
2166
|
const out = Buffer.from(await response.arrayBuffer());
|
|
1609
2167
|
res.statusCode = response.status;
|
|
@@ -1630,22 +2188,79 @@ function createSwaggerizer(options = {}) {
|
|
|
1630
2188
|
const spec = specs.get(op.specId);
|
|
1631
2189
|
const cfg = configFor(op.specId);
|
|
1632
2190
|
const override = parseScenarioHeader(req.headers[SCENARIO_HEADER]);
|
|
1633
|
-
const
|
|
2191
|
+
const mode = validationModeFor(op, req.headers[VALIDATION_HEADER]);
|
|
2192
|
+
const resolved = resolveResponse(op, override);
|
|
1634
2193
|
const custom = lookupByOperation(cfg.handlers, op);
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2194
|
+
const wantsHandler = Boolean(custom) && !override && !op.modified;
|
|
2195
|
+
const sendsBody = method !== "get" && method !== "head";
|
|
2196
|
+
// The request stream can only be read once: buffer it when validation, a custom handler or the
|
|
2197
|
+
// proxy needs it, then hand the same buffer to each of them.
|
|
2198
|
+
const buffered = sendsBody && (mode !== "off" || wantsHandler || resolved.kind === "proxy")
|
|
2199
|
+
? await readBody(req)
|
|
2200
|
+
: undefined;
|
|
2201
|
+
const contentType = req.headers["content-type"];
|
|
2202
|
+
// What a custom handler has always received: parsed JSON, or the raw text when it is not JSON.
|
|
2203
|
+
let handlerBody;
|
|
2204
|
+
if (buffered && buffered.length > 0) {
|
|
2205
|
+
const text = buffered.toString("utf8");
|
|
2206
|
+
try {
|
|
2207
|
+
handlerBody = text.trim() ? JSON.parse(text) : undefined;
|
|
2208
|
+
}
|
|
2209
|
+
catch {
|
|
2210
|
+
handlerBody = text;
|
|
1645
2211
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2212
|
+
}
|
|
2213
|
+
let validationBody;
|
|
2214
|
+
let bodyError;
|
|
2215
|
+
// A whitespace-only body is no body, for validation as for the handler.
|
|
2216
|
+
if (mode !== "off" && buffered && buffered.toString("utf8").trim() !== "") {
|
|
2217
|
+
try {
|
|
2218
|
+
validationBody = parseRequestBody(buffered, contentType);
|
|
2219
|
+
}
|
|
2220
|
+
catch (err) {
|
|
2221
|
+
bodyError = err?.message ? String(err.message) : String(err);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const pathParams = candidatePaths(url.pathname, spec.basePaths, cfg.aliases || {})
|
|
2225
|
+
.map(p => extractParams(op.route, p))
|
|
2226
|
+
.find(p => Object.keys(p).length > 0) ?? {};
|
|
2227
|
+
let summary;
|
|
2228
|
+
if (mode !== "off") {
|
|
2229
|
+
summary = validatorFor(spec).validateRequest(op, {
|
|
2230
|
+
params: pathParams,
|
|
2231
|
+
query: url.searchParams,
|
|
2232
|
+
headers: req.headers,
|
|
2233
|
+
...(validationBody !== undefined ? { body: validationBody } : {}),
|
|
2234
|
+
...(bodyError ? { bodyError } : {}),
|
|
2235
|
+
...(contentType ? { contentType } : {})
|
|
2236
|
+
});
|
|
2237
|
+
if (summary.request.length === 0 && !summary.skipped)
|
|
2238
|
+
summary = undefined;
|
|
2239
|
+
}
|
|
2240
|
+
const entry = {
|
|
2241
|
+
method: method.toUpperCase(),
|
|
2242
|
+
path: url.pathname + url.search,
|
|
2243
|
+
route: op.route,
|
|
2244
|
+
specId: op.specId,
|
|
2245
|
+
source: "server",
|
|
2246
|
+
...(summary ? { validation: summary } : {})
|
|
2247
|
+
};
|
|
2248
|
+
if (summary && mode === "enforce" && summary.request.length > 0) {
|
|
2249
|
+
res.setHeader("X-Swaggerizer-Validation", "failed");
|
|
2250
|
+
sendJson(res, 400, {
|
|
2251
|
+
error: "Request validation failed",
|
|
2252
|
+
operation: `${op.method.toUpperCase()} ${op.route}`,
|
|
2253
|
+
specId: op.specId,
|
|
2254
|
+
issues: summary.request
|
|
2255
|
+
});
|
|
2256
|
+
recordRequest({ ...entry, status: 400, example: "validation", ms: Date.now() - started });
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2259
|
+
if (summary)
|
|
2260
|
+
res.setHeader("X-Swaggerizer-Validation", "warning");
|
|
2261
|
+
if (custom && wantsHandler) {
|
|
2262
|
+
const body = handlerBody;
|
|
2263
|
+
const params = pathParams;
|
|
1649
2264
|
const headers = Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v ?? "")]));
|
|
1650
2265
|
let outcome;
|
|
1651
2266
|
try {
|
|
@@ -1672,7 +2287,6 @@ function createSwaggerizer(options = {}) {
|
|
|
1672
2287
|
return true;
|
|
1673
2288
|
}
|
|
1674
2289
|
}
|
|
1675
|
-
const resolved = resolveResponse(op, override);
|
|
1676
2290
|
if (resolved.delay > 0)
|
|
1677
2291
|
await sleep(resolved.delay);
|
|
1678
2292
|
if (resolved.kind === "proxy") {
|
|
@@ -1684,7 +2298,7 @@ function createSwaggerizer(options = {}) {
|
|
|
1684
2298
|
}
|
|
1685
2299
|
const relative = relativeToBase(url.pathname, spec.basePaths, cfg.aliases || {});
|
|
1686
2300
|
try {
|
|
1687
|
-
const status = await proxy(req, res, target, relative, url.search);
|
|
2301
|
+
const status = await proxy(req, res, target, relative, url.search, buffered);
|
|
1688
2302
|
recordRequest({ ...entry, status, example: "proxy", ms: Date.now() - started });
|
|
1689
2303
|
}
|
|
1690
2304
|
catch (err) {
|
|
@@ -1722,14 +2336,15 @@ function createSwaggerizer(options = {}) {
|
|
|
1722
2336
|
reload,
|
|
1723
2337
|
getRequestLog,
|
|
1724
2338
|
recordRequest,
|
|
1725
|
-
getSpecDocument: id => specs.get(id)?.doc
|
|
2339
|
+
getSpecDocument: id => specs.get(id)?.doc,
|
|
2340
|
+
getValidation: validationInfo
|
|
1726
2341
|
});
|
|
1727
2342
|
function handler(req, res, next) {
|
|
1728
2343
|
if (options.cors !== false) {
|
|
1729
2344
|
res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*");
|
|
1730
2345
|
res.setHeader("Vary", "Origin");
|
|
1731
2346
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1732
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Swaggerizer-Scenario, X-Swaggerizer-Proxied");
|
|
2347
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Swaggerizer-Scenario, X-Swaggerizer-Proxied, X-Swaggerizer-Validation");
|
|
1733
2348
|
if (req.method === "OPTIONS" && req.headers["access-control-request-method"]) {
|
|
1734
2349
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS");
|
|
1735
2350
|
res.setHeader("Access-Control-Allow-Headers", req.headers["access-control-request-headers"] || "*");
|
|
@@ -1823,6 +2438,7 @@ function createSwaggerizer(options = {}) {
|
|
|
1823
2438
|
getRequestLog,
|
|
1824
2439
|
recordRequest,
|
|
1825
2440
|
getSpecDocument: id => specs.get(id)?.doc,
|
|
2441
|
+
getValidationInfo: validationInfo,
|
|
1826
2442
|
listen,
|
|
1827
2443
|
close,
|
|
1828
2444
|
endpoint,
|
|
@@ -2339,5 +2955,5 @@ async function pull(options) {
|
|
|
2339
2955
|
return result;
|
|
2340
2956
|
}
|
|
2341
2957
|
|
|
2342
|
-
export { CONTRACT_FILE, EMPTY, GENERATED, HTTP_METHODS, SCENARIO_HEADER, WRAPPER_FILE, aliasesFromTsconfig, analyseSpecFile, applyOverlays, applySelection, basePathsOf, buildCatalogue, buildContract, buildOperationStates, candidatePaths, checkChanges, contractKey, createHandlerContext, createSwaggerizer, diffContracts, exceedsThreshold, extractParams, findConfigFile, findMockAggregate, formatReport, generate, hasChanges, isHandlerResult, listSpecFiles, loadConfig, loadSpecFile, lookupByOperation, matchOperation, moveMocks, normaliseMediaType, operationKey, outputDirs, parseScenarioHeader, pull, relativeToBase, resolveResponse, resolveSpecInputs, resolveTargets, sampleFromSchema, serverUrlOf, specIdFor, startSwaggerizer, statusForCode, unmatchedKeys };
|
|
2958
|
+
export { CONTRACT_FILE, EMPTY, GENERATED, HTTP_METHODS, SCENARIO_HEADER, VALIDATION_HEADER, WRAPPER_FILE, aliasesFromTsconfig, analyseSpecFile, applyOverlays, applySelection, basePathsOf, buildCatalogue, buildContract, buildOperationStates, candidatePaths, checkChanges, contractKey, createHandlerContext, createSwaggerizer, createValidator, diffContracts, exceedsThreshold, extractParams, findConfigFile, findMockAggregate, formatIssues, formatReport, generate, hasChanges, isHandlerResult, listSpecFiles, loadConfig, loadSpecFile, lookupByOperation, matchOperation, matchesIgnore, moveMocks, normaliseMediaType, operationKey, outputDirs, parseScenarioHeader, parseValidationMode, pull, relativeToBase, resolveResponse, resolveSpecInputs, resolveTargets, sampleFromSchema, serverUrlOf, specIdFor, startSwaggerizer, statusForCode, unmatchedKeys };
|
|
2343
2959
|
//# sourceMappingURL=index.mjs.map
|