@schmock/cli 1.13.0 → 2.0.1
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/dist/bin.js +259 -52
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -3
- package/dist/index.js +259 -52
- package/package.json +3 -3
package/dist/bin.js
CHANGED
|
@@ -17735,6 +17735,7 @@ import { parseArgs } from "node:util";
|
|
|
17735
17735
|
import { createServer } from "node:http";
|
|
17736
17736
|
|
|
17737
17737
|
// ../core/src/constants.ts
|
|
17738
|
+
var ROUTE_NOT_FOUND_CODE = "ROUTE_NOT_FOUND";
|
|
17738
17739
|
var HTTP_METHODS = [
|
|
17739
17740
|
"GET",
|
|
17740
17741
|
"POST",
|
|
@@ -17761,6 +17762,10 @@ function toRouteKey(method, path) {
|
|
|
17761
17762
|
const key = `${method} ${path}`;
|
|
17762
17763
|
return key;
|
|
17763
17764
|
}
|
|
17765
|
+
function isRouteNotFound(response) {
|
|
17766
|
+
const { status, body } = response;
|
|
17767
|
+
return status === 404 && body !== null && typeof body === "object" && "code" in body && body.code === ROUTE_NOT_FOUND_CODE;
|
|
17768
|
+
}
|
|
17764
17769
|
function isStatusTuple(value) {
|
|
17765
17770
|
return Array.isArray(value) && (value.length === 2 || value.length === 3) && typeof value[0] === "number" && value[0] >= 100 && value[0] <= 599;
|
|
17766
17771
|
}
|
|
@@ -17894,6 +17899,177 @@ function writeSchmockResponse(res, response, extraHeaders) {
|
|
|
17894
17899
|
res.end(responseBody);
|
|
17895
17900
|
}
|
|
17896
17901
|
|
|
17902
|
+
// ../core/src/interceptor.ts
|
|
17903
|
+
function extractPathname(url) {
|
|
17904
|
+
const queryStart = url.indexOf("?");
|
|
17905
|
+
const urlWithoutQuery = queryStart === -1 ? url : url.slice(0, queryStart);
|
|
17906
|
+
if (urlWithoutQuery.includes("://")) {
|
|
17907
|
+
try {
|
|
17908
|
+
return new URL(urlWithoutQuery).pathname;
|
|
17909
|
+
} catch {}
|
|
17910
|
+
}
|
|
17911
|
+
if (!urlWithoutQuery.startsWith("/")) {
|
|
17912
|
+
return `/${urlWithoutQuery}`;
|
|
17913
|
+
}
|
|
17914
|
+
return urlWithoutQuery;
|
|
17915
|
+
}
|
|
17916
|
+
function extractQuery(url) {
|
|
17917
|
+
const queryStart = url.indexOf("?");
|
|
17918
|
+
if (queryStart === -1)
|
|
17919
|
+
return {};
|
|
17920
|
+
const params = new URLSearchParams(url.slice(queryStart + 1));
|
|
17921
|
+
const result = {};
|
|
17922
|
+
params.forEach((value, key) => {
|
|
17923
|
+
result[key] = value;
|
|
17924
|
+
});
|
|
17925
|
+
return result;
|
|
17926
|
+
}
|
|
17927
|
+
function extractHeaders(input, init) {
|
|
17928
|
+
const headers = {};
|
|
17929
|
+
const raw = init?.headers ?? (input instanceof Request ? input.headers : undefined);
|
|
17930
|
+
if (!raw)
|
|
17931
|
+
return headers;
|
|
17932
|
+
if (raw instanceof Headers) {
|
|
17933
|
+
raw.forEach((value, key) => {
|
|
17934
|
+
headers[key] = value;
|
|
17935
|
+
});
|
|
17936
|
+
} else if (Array.isArray(raw)) {
|
|
17937
|
+
for (const [key, value] of raw) {
|
|
17938
|
+
headers[key.toLowerCase()] = value;
|
|
17939
|
+
}
|
|
17940
|
+
} else {
|
|
17941
|
+
for (const key of Object.keys(raw)) {
|
|
17942
|
+
headers[key.toLowerCase()] = raw[key];
|
|
17943
|
+
}
|
|
17944
|
+
}
|
|
17945
|
+
return headers;
|
|
17946
|
+
}
|
|
17947
|
+
async function extractBody(input, init) {
|
|
17948
|
+
const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
|
|
17949
|
+
if (bodyInit === null || bodyInit === undefined)
|
|
17950
|
+
return;
|
|
17951
|
+
if (typeof bodyInit === "string") {
|
|
17952
|
+
try {
|
|
17953
|
+
return JSON.parse(bodyInit);
|
|
17954
|
+
} catch {
|
|
17955
|
+
return bodyInit;
|
|
17956
|
+
}
|
|
17957
|
+
}
|
|
17958
|
+
if (bodyInit instanceof URLSearchParams) {
|
|
17959
|
+
const result = {};
|
|
17960
|
+
bodyInit.forEach((value, key) => {
|
|
17961
|
+
result[key] = value;
|
|
17962
|
+
});
|
|
17963
|
+
return result;
|
|
17964
|
+
}
|
|
17965
|
+
if (input instanceof Request && !init?.body && input.body) {
|
|
17966
|
+
try {
|
|
17967
|
+
return await input.clone().json();
|
|
17968
|
+
} catch {
|
|
17969
|
+
try {
|
|
17970
|
+
return await input.clone().text();
|
|
17971
|
+
} catch {
|
|
17972
|
+
return;
|
|
17973
|
+
}
|
|
17974
|
+
}
|
|
17975
|
+
}
|
|
17976
|
+
return;
|
|
17977
|
+
}
|
|
17978
|
+
function createFetchInterceptor(handle, options = {}) {
|
|
17979
|
+
const {
|
|
17980
|
+
baseUrl,
|
|
17981
|
+
passthrough = true,
|
|
17982
|
+
beforeRequest,
|
|
17983
|
+
beforeResponse,
|
|
17984
|
+
errorFormatter
|
|
17985
|
+
} = options;
|
|
17986
|
+
const originalFetch = globalThis.fetch;
|
|
17987
|
+
let active = true;
|
|
17988
|
+
globalThis.fetch = async (input, init) => {
|
|
17989
|
+
const urlString = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
|
|
17990
|
+
const path = extractPathname(urlString);
|
|
17991
|
+
if (baseUrl) {
|
|
17992
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
17993
|
+
const isMatch = path === normalizedBase || path.startsWith(`${normalizedBase}/`);
|
|
17994
|
+
if (!isMatch) {
|
|
17995
|
+
return originalFetch(input, init);
|
|
17996
|
+
}
|
|
17997
|
+
}
|
|
17998
|
+
const method = input instanceof Request ? input.method : init?.method ?? "GET";
|
|
17999
|
+
const headers = extractHeaders(input, init);
|
|
18000
|
+
const query = extractQuery(urlString);
|
|
18001
|
+
const body = await extractBody(input, init);
|
|
18002
|
+
let adapterRequest = {
|
|
18003
|
+
method,
|
|
18004
|
+
path,
|
|
18005
|
+
headers,
|
|
18006
|
+
body,
|
|
18007
|
+
query
|
|
18008
|
+
};
|
|
18009
|
+
try {
|
|
18010
|
+
if (beforeRequest) {
|
|
18011
|
+
const modified = await beforeRequest(adapterRequest);
|
|
18012
|
+
if (modified) {
|
|
18013
|
+
adapterRequest = modified;
|
|
18014
|
+
}
|
|
18015
|
+
}
|
|
18016
|
+
const schmockResponse = await handle(toHttpMethod(adapterRequest.method), adapterRequest.path, {
|
|
18017
|
+
headers: adapterRequest.headers,
|
|
18018
|
+
body: adapterRequest.body,
|
|
18019
|
+
query: adapterRequest.query
|
|
18020
|
+
});
|
|
18021
|
+
if (isRouteNotFound(schmockResponse)) {
|
|
18022
|
+
if (passthrough) {
|
|
18023
|
+
return originalFetch(input, init);
|
|
18024
|
+
}
|
|
18025
|
+
return new Response(JSON.stringify({
|
|
18026
|
+
error: "No matching mock route found",
|
|
18027
|
+
code: "ROUTE_NOT_FOUND"
|
|
18028
|
+
}), {
|
|
18029
|
+
status: 404,
|
|
18030
|
+
headers: { "content-type": "application/json" }
|
|
18031
|
+
});
|
|
18032
|
+
}
|
|
18033
|
+
let response = schmockResponse;
|
|
18034
|
+
if (beforeResponse) {
|
|
18035
|
+
const modified = await beforeResponse(response, adapterRequest);
|
|
18036
|
+
if (modified) {
|
|
18037
|
+
response = modified;
|
|
18038
|
+
}
|
|
18039
|
+
}
|
|
18040
|
+
const responseHeaders = new Headers(response.headers);
|
|
18041
|
+
if (!responseHeaders.has("content-type") && response.body !== null && response.body !== undefined) {
|
|
18042
|
+
responseHeaders.set("content-type", "application/json");
|
|
18043
|
+
}
|
|
18044
|
+
const responseBody = response.body === null || response.body === undefined ? null : typeof response.body === "string" ? response.body : JSON.stringify(response.body);
|
|
18045
|
+
return new Response(responseBody, {
|
|
18046
|
+
status: response.status,
|
|
18047
|
+
headers: responseHeaders
|
|
18048
|
+
});
|
|
18049
|
+
} catch (error) {
|
|
18050
|
+
if (errorFormatter) {
|
|
18051
|
+
const formatted = errorFormatter(error instanceof Error ? error : new Error(String(error)));
|
|
18052
|
+
return new Response(JSON.stringify(formatted), {
|
|
18053
|
+
status: 500,
|
|
18054
|
+
headers: { "content-type": "application/json" }
|
|
18055
|
+
});
|
|
18056
|
+
}
|
|
18057
|
+
throw error;
|
|
18058
|
+
}
|
|
18059
|
+
};
|
|
18060
|
+
return {
|
|
18061
|
+
restore() {
|
|
18062
|
+
if (active) {
|
|
18063
|
+
globalThis.fetch = originalFetch;
|
|
18064
|
+
active = false;
|
|
18065
|
+
}
|
|
18066
|
+
},
|
|
18067
|
+
get active() {
|
|
18068
|
+
return active;
|
|
18069
|
+
}
|
|
18070
|
+
};
|
|
18071
|
+
}
|
|
18072
|
+
|
|
17897
18073
|
// ../core/src/parser.ts
|
|
17898
18074
|
function parseRouteKey(routeKey) {
|
|
17899
18075
|
const match = routeKey.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (.+)$/);
|
|
@@ -17928,7 +18104,7 @@ async function runPluginPipeline(plugins, context, initialResponse, logger) {
|
|
|
17928
18104
|
logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
17929
18105
|
try {
|
|
17930
18106
|
const result = await plugin.process(currentContext, response);
|
|
17931
|
-
if (!result
|
|
18107
|
+
if (!result?.context) {
|
|
17932
18108
|
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
17933
18109
|
}
|
|
17934
18110
|
currentContext = result.context;
|
|
@@ -18078,6 +18254,7 @@ class CallableMockInstance {
|
|
|
18078
18254
|
callableRef;
|
|
18079
18255
|
server;
|
|
18080
18256
|
serverInfo;
|
|
18257
|
+
interceptHandle = null;
|
|
18081
18258
|
listeners = new Map;
|
|
18082
18259
|
constructor(globalConfig = {}) {
|
|
18083
18260
|
this.globalConfig = globalConfig;
|
|
@@ -18210,6 +18387,8 @@ class CallableMockInstance {
|
|
|
18210
18387
|
}
|
|
18211
18388
|
}
|
|
18212
18389
|
reset() {
|
|
18390
|
+
this.interceptHandle?.restore();
|
|
18391
|
+
this.interceptHandle = null;
|
|
18213
18392
|
this.close();
|
|
18214
18393
|
this.routes = [];
|
|
18215
18394
|
this.staticRoutes.clear();
|
|
@@ -18280,11 +18459,19 @@ class CallableMockInstance {
|
|
|
18280
18459
|
if (!this.server) {
|
|
18281
18460
|
return;
|
|
18282
18461
|
}
|
|
18462
|
+
this.server.closeAllConnections();
|
|
18283
18463
|
this.server.close();
|
|
18284
18464
|
this.server = undefined;
|
|
18285
18465
|
this.serverInfo = undefined;
|
|
18286
18466
|
this.logger.log("server", "Server stopped");
|
|
18287
18467
|
}
|
|
18468
|
+
intercept(options) {
|
|
18469
|
+
if (this.interceptHandle?.active) {
|
|
18470
|
+
throw new SchmockError("Already intercepting. Call restore() first.", "ALREADY_INTERCEPTING");
|
|
18471
|
+
}
|
|
18472
|
+
this.interceptHandle = createFetchInterceptor((method, path, opts) => this.handle(method, path, opts), options);
|
|
18473
|
+
return this.interceptHandle;
|
|
18474
|
+
}
|
|
18288
18475
|
async handle(method, path, options) {
|
|
18289
18476
|
const handleStart = performance.now();
|
|
18290
18477
|
const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
|
|
@@ -18475,13 +18662,14 @@ function schmock(config) {
|
|
|
18475
18662
|
getRoutes: instance.getRoutes.bind(instance),
|
|
18476
18663
|
getState: instance.getState.bind(instance),
|
|
18477
18664
|
listen: instance.listen.bind(instance),
|
|
18478
|
-
close: instance.close.bind(instance)
|
|
18665
|
+
close: instance.close.bind(instance),
|
|
18666
|
+
intercept: (options) => instance.intercept(options)
|
|
18479
18667
|
});
|
|
18480
18668
|
instance.setCallableRef(callableInstance);
|
|
18481
18669
|
return callableInstance;
|
|
18482
18670
|
}
|
|
18483
18671
|
// ../openapi/package.json
|
|
18484
|
-
var version = "
|
|
18672
|
+
var version = "2.0.1";
|
|
18485
18673
|
|
|
18486
18674
|
// ../openapi/src/utils.ts
|
|
18487
18675
|
function isRecord(value) {
|
|
@@ -18499,14 +18687,16 @@ function getRouteCallbacks(route) {
|
|
|
18499
18687
|
function fireCallbacks(callbacks, context, response) {
|
|
18500
18688
|
for (const callback of callbacks) {
|
|
18501
18689
|
const url = resolveCallbackUrl(callback.urlExpression, context, response);
|
|
18502
|
-
if (!url
|
|
18690
|
+
if (!url?.startsWith("http"))
|
|
18503
18691
|
continue;
|
|
18504
18692
|
const body = Array.isArray(response) ? response[1] : response;
|
|
18505
18693
|
fetch(url, {
|
|
18506
18694
|
method: callback.method,
|
|
18507
18695
|
headers: { "content-type": "application/json" },
|
|
18508
18696
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
18509
|
-
}).catch(() => {
|
|
18697
|
+
}).catch((error) => {
|
|
18698
|
+
console.warn(`[@schmock/openapi] Callback ${callback.method} ${url} failed:`, error instanceof Error ? error.message : error);
|
|
18699
|
+
});
|
|
18510
18700
|
}
|
|
18511
18701
|
}
|
|
18512
18702
|
function resolveCallbackUrl(expression, context, response) {
|
|
@@ -18552,7 +18742,6 @@ function resolveJsonPointer(obj, pointer) {
|
|
|
18552
18742
|
|
|
18553
18743
|
// ../openapi/src/generators.ts
|
|
18554
18744
|
import { randomUUID } from "node:crypto";
|
|
18555
|
-
|
|
18556
18745
|
// ../faker/src/constants.ts
|
|
18557
18746
|
var MAX_ARRAY_SIZE = 1e4;
|
|
18558
18747
|
var MAX_NESTING_DEPTH = 10;
|
|
@@ -22437,7 +22626,9 @@ function determineArrayCount(schema, explicitCount, random = Math.random) {
|
|
|
22437
22626
|
return explicitCount;
|
|
22438
22627
|
}
|
|
22439
22628
|
if (schema.minItems !== undefined && schema.maxItems !== undefined) {
|
|
22440
|
-
|
|
22629
|
+
const min = schema.minItems;
|
|
22630
|
+
const max = Math.max(min, schema.maxItems);
|
|
22631
|
+
return Math.floor(random() * (max - min + 1)) + min;
|
|
22441
22632
|
}
|
|
22442
22633
|
if (schema.minItems !== undefined) {
|
|
22443
22634
|
return Math.max(schema.minItems, DEFAULT_ARRAY_COUNT);
|
|
@@ -22450,6 +22641,8 @@ function determineArrayCount(schema, explicitCount, random = Math.random) {
|
|
|
22450
22641
|
function applyOverrides(data, overrides, params, state, query) {
|
|
22451
22642
|
if (!overrides)
|
|
22452
22643
|
return data;
|
|
22644
|
+
if (typeof data !== "object" || data === null)
|
|
22645
|
+
return data;
|
|
22453
22646
|
const result = structuredClone(data);
|
|
22454
22647
|
for (const [key, value] of Object.entries(overrides)) {
|
|
22455
22648
|
if (DANGEROUS_KEYS.has(key))
|
|
@@ -22458,10 +22651,11 @@ function applyOverrides(data, overrides, params, state, query) {
|
|
|
22458
22651
|
setNestedProperty(result, key, value, { params, state, query });
|
|
22459
22652
|
} else {
|
|
22460
22653
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
22654
|
+
const nested = value;
|
|
22461
22655
|
if (result[key] && typeof result[key] === "object") {
|
|
22462
|
-
result[key] = applyOverrides(result[key],
|
|
22656
|
+
result[key] = applyOverrides(result[key], nested, params, state, query);
|
|
22463
22657
|
} else {
|
|
22464
|
-
result[key] = applyOverrides({},
|
|
22658
|
+
result[key] = applyOverrides({}, nested, params, state, query);
|
|
22465
22659
|
}
|
|
22466
22660
|
} else if (typeof value === "string" && value.includes("{{")) {
|
|
22467
22661
|
result[key] = processTemplate(value, { params, state, query });
|
|
@@ -22493,34 +22687,28 @@ function setNestedProperty(obj, path, value, context) {
|
|
|
22493
22687
|
current[finalKey] = value;
|
|
22494
22688
|
}
|
|
22495
22689
|
}
|
|
22690
|
+
function resolveTemplatePath(context, expression) {
|
|
22691
|
+
const parts = expression.trim().split(".");
|
|
22692
|
+
let result = context;
|
|
22693
|
+
for (const part of parts) {
|
|
22694
|
+
if (result && typeof result === "object") {
|
|
22695
|
+
result = result[part];
|
|
22696
|
+
} else {
|
|
22697
|
+
return;
|
|
22698
|
+
}
|
|
22699
|
+
}
|
|
22700
|
+
return result;
|
|
22701
|
+
}
|
|
22496
22702
|
function processTemplate(template, context) {
|
|
22497
22703
|
const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
|
|
22498
22704
|
if (singleTemplateMatch) {
|
|
22499
|
-
const
|
|
22500
|
-
const parts = expression.trim().split(".");
|
|
22501
|
-
let result = context;
|
|
22502
|
-
for (const part of parts) {
|
|
22503
|
-
if (result && typeof result === "object") {
|
|
22504
|
-
result = result[part];
|
|
22505
|
-
} else {
|
|
22506
|
-
return template;
|
|
22507
|
-
}
|
|
22508
|
-
}
|
|
22705
|
+
const result = resolveTemplatePath(context, singleTemplateMatch[1]);
|
|
22509
22706
|
return result !== undefined ? result : template;
|
|
22510
22707
|
}
|
|
22511
|
-
|
|
22512
|
-
const
|
|
22513
|
-
let result = context;
|
|
22514
|
-
for (const part of parts) {
|
|
22515
|
-
if (result && typeof result === "object") {
|
|
22516
|
-
result = result[part];
|
|
22517
|
-
} else {
|
|
22518
|
-
return match;
|
|
22519
|
-
}
|
|
22520
|
-
}
|
|
22708
|
+
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expression) => {
|
|
22709
|
+
const result = resolveTemplatePath(context, expression);
|
|
22521
22710
|
return result !== undefined ? String(result) : match;
|
|
22522
22711
|
});
|
|
22523
|
-
return processed;
|
|
22524
22712
|
}
|
|
22525
22713
|
|
|
22526
22714
|
// ../faker/src/field-mappings.ts
|
|
@@ -23992,7 +24180,6 @@ function enhanceFieldSchema(fieldName, fieldSchema) {
|
|
|
23992
24180
|
async function generateFromSchema(options) {
|
|
23993
24181
|
const { schema, count, overrides, params, state, query, seed } = options;
|
|
23994
24182
|
validateSchema(schema);
|
|
23995
|
-
let generated;
|
|
23996
24183
|
if (schema.type === "array" && schema.items) {
|
|
23997
24184
|
const itemCount = determineArrayCount(schema, count);
|
|
23998
24185
|
if (itemCount > MAX_ARRAY_SIZE) {
|
|
@@ -24004,38 +24191,39 @@ async function generateFromSchema(options) {
|
|
|
24004
24191
|
}
|
|
24005
24192
|
const itemSchema = rawItemSchema;
|
|
24006
24193
|
const enhancedItemSchema = enhanceSchemaWithSmartMapping(itemSchema);
|
|
24007
|
-
|
|
24194
|
+
const items = [];
|
|
24008
24195
|
for (let i3 = 0;i3 < itemCount; i3++) {
|
|
24009
24196
|
let item = await generateWithJsf(enhancedItemSchema, seed);
|
|
24010
24197
|
item = postProcessGenerated(item, enhancedItemSchema);
|
|
24011
24198
|
item = applyOverrides(item, overrides, params, state, query);
|
|
24012
|
-
|
|
24199
|
+
items.push(item);
|
|
24013
24200
|
}
|
|
24014
|
-
|
|
24015
|
-
const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
|
|
24016
|
-
generated = await generateWithJsf(enhancedSchema, seed);
|
|
24017
|
-
generated = postProcessGenerated(generated, enhancedSchema);
|
|
24018
|
-
generated = applyOverrides(generated, overrides, params, state, query);
|
|
24201
|
+
return items;
|
|
24019
24202
|
}
|
|
24203
|
+
const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
|
|
24204
|
+
let generated = await generateWithJsf(enhancedSchema, seed);
|
|
24205
|
+
generated = postProcessGenerated(generated, enhancedSchema);
|
|
24206
|
+
generated = applyOverrides(generated, overrides, params, state, query);
|
|
24020
24207
|
return generated;
|
|
24021
24208
|
}
|
|
24022
24209
|
function postProcessGenerated(data, schema) {
|
|
24023
24210
|
if (data === null || data === undefined || !schema || typeof schema !== "object") {
|
|
24024
24211
|
return data;
|
|
24025
24212
|
}
|
|
24026
|
-
const
|
|
24027
|
-
if (
|
|
24213
|
+
const schemaExt = schema;
|
|
24214
|
+
if (schemaExt.schmockNullable === true) {
|
|
24028
24215
|
if (Math.random() < NULLABLE_NULL_PROBABILITY) {
|
|
24029
24216
|
return null;
|
|
24030
24217
|
}
|
|
24031
24218
|
}
|
|
24032
|
-
if (schema.type === "boolean" && typeof
|
|
24033
|
-
return Math.random() <
|
|
24219
|
+
if (schema.type === "boolean" && typeof schemaExt.schmockTrueProbability === "number") {
|
|
24220
|
+
return Math.random() < schemaExt.schmockTrueProbability;
|
|
24034
24221
|
}
|
|
24035
24222
|
if (typeof data === "object" && !Array.isArray(data) && schema.properties) {
|
|
24223
|
+
const record = data;
|
|
24036
24224
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
24037
|
-
if (key in
|
|
24038
|
-
|
|
24225
|
+
if (key in record && isJSONSchema7(propSchema)) {
|
|
24226
|
+
record[key] = postProcessGenerated(record[key], propSchema);
|
|
24039
24227
|
}
|
|
24040
24228
|
}
|
|
24041
24229
|
}
|
|
@@ -24296,7 +24484,9 @@ async function generateErrorResponse(status, meta) {
|
|
|
24296
24484
|
try {
|
|
24297
24485
|
const body = await generateFromSchema({ schema: errorSchema });
|
|
24298
24486
|
return toTuple(status, body);
|
|
24299
|
-
} catch {
|
|
24487
|
+
} catch (error) {
|
|
24488
|
+
console.warn(`[@schmock/openapi] Error schema generation failed for status ${status}:`, error instanceof Error ? error.message : error);
|
|
24489
|
+
}
|
|
24300
24490
|
}
|
|
24301
24491
|
const defaults = {
|
|
24302
24492
|
404: { error: "Not found", code: "NOT_FOUND" },
|
|
@@ -24308,7 +24498,8 @@ async function generateErrorResponse(status, meta) {
|
|
|
24308
24498
|
async function generateWrapperSkeleton(schema) {
|
|
24309
24499
|
try {
|
|
24310
24500
|
return await generateFromSchema({ schema });
|
|
24311
|
-
} catch {
|
|
24501
|
+
} catch (error) {
|
|
24502
|
+
console.warn("[@schmock/openapi] Wrapper skeleton generation failed:", error instanceof Error ? error.message : error);
|
|
24312
24503
|
return {};
|
|
24313
24504
|
}
|
|
24314
24505
|
}
|
|
@@ -24887,7 +25078,7 @@ async function parseSpec(source) {
|
|
|
24887
25078
|
}
|
|
24888
25079
|
const isSwagger2 = "swagger" in api && typeof api.swagger === "string";
|
|
24889
25080
|
const title = api.info?.title ?? "Untitled";
|
|
24890
|
-
const
|
|
25081
|
+
const version3 = api.info?.version ?? "0.0.0";
|
|
24891
25082
|
let basePath = "";
|
|
24892
25083
|
if (isSwagger2 && "basePath" in api) {
|
|
24893
25084
|
const bp = api.basePath;
|
|
@@ -24912,7 +25103,7 @@ async function parseSpec(source) {
|
|
|
24912
25103
|
const paths = [];
|
|
24913
25104
|
const rawPaths = "paths" in api && isRecord(api.paths) ? api.paths : undefined;
|
|
24914
25105
|
if (!rawPaths) {
|
|
24915
|
-
return { title, version:
|
|
25106
|
+
return { title, version: version3, basePath, paths, securitySchemes, globalSecurity };
|
|
24916
25107
|
}
|
|
24917
25108
|
for (const [pathTemplate, pathItemRaw] of Object.entries(rawPaths)) {
|
|
24918
25109
|
if (!isRecord(pathItemRaw))
|
|
@@ -24953,7 +25144,7 @@ async function parseSpec(source) {
|
|
|
24953
25144
|
});
|
|
24954
25145
|
}
|
|
24955
25146
|
}
|
|
24956
|
-
return { title, version:
|
|
25147
|
+
return { title, version: version3, basePath, paths, securitySchemes, globalSecurity };
|
|
24957
25148
|
}
|
|
24958
25149
|
function isValidParamLocation(location2, isSwagger2) {
|
|
24959
25150
|
const validLocations = isSwagger2 ? ["path", "query", "header", "body"] : ["path", "query", "header"];
|
|
@@ -25484,7 +25675,8 @@ async function generateResponseBody(schema, seed, onSchema, context) {
|
|
|
25484
25675
|
}
|
|
25485
25676
|
try {
|
|
25486
25677
|
return await generateFromSchema({ schema: finalSchema, seed });
|
|
25487
|
-
} catch {
|
|
25678
|
+
} catch (error) {
|
|
25679
|
+
console.warn("[@schmock/openapi] Response body generation failed:", error instanceof Error ? error.message : error);
|
|
25488
25680
|
return {};
|
|
25489
25681
|
}
|
|
25490
25682
|
}
|
|
@@ -25698,7 +25890,15 @@ async function createCliServer(options) {
|
|
|
25698
25890
|
collectBody(req, headers).then((body) => mock.handle(method, path, { headers, body, query }).then((schmockResponse) => {
|
|
25699
25891
|
const extra = cors ? CORS_HEADERS : undefined;
|
|
25700
25892
|
writeSchmockResponse(res, schmockResponse, extra);
|
|
25701
|
-
}))
|
|
25893
|
+
})).catch((error) => {
|
|
25894
|
+
if (!res.headersSent) {
|
|
25895
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
25896
|
+
}
|
|
25897
|
+
res.end(JSON.stringify({
|
|
25898
|
+
error: error instanceof Error ? error.message : "Internal Server Error",
|
|
25899
|
+
code: "SERVER_ERROR"
|
|
25900
|
+
}));
|
|
25901
|
+
});
|
|
25702
25902
|
});
|
|
25703
25903
|
return new Promise((resolve, reject) => {
|
|
25704
25904
|
httpServer.on("error", reject);
|
|
@@ -25716,6 +25916,13 @@ async function createCliServer(options) {
|
|
|
25716
25916
|
});
|
|
25717
25917
|
});
|
|
25718
25918
|
}
|
|
25919
|
+
function validatePort(value) {
|
|
25920
|
+
const port = Number(value);
|
|
25921
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
25922
|
+
throw new Error(`Invalid port "${value}". Port must be an integer between 0 and 65535.`);
|
|
25923
|
+
}
|
|
25924
|
+
return port;
|
|
25925
|
+
}
|
|
25719
25926
|
function parseCliArgs(args) {
|
|
25720
25927
|
const { values, positionals } = parseArgs({
|
|
25721
25928
|
args,
|
|
@@ -25738,7 +25945,7 @@ function parseCliArgs(args) {
|
|
|
25738
25945
|
const spec = values.spec ?? positionals[0] ?? "";
|
|
25739
25946
|
return {
|
|
25740
25947
|
spec,
|
|
25741
|
-
port: values.port ?
|
|
25948
|
+
port: values.port ? validatePort(values.port) : undefined,
|
|
25742
25949
|
hostname: values.hostname,
|
|
25743
25950
|
seed: values.seed,
|
|
25744
25951
|
cors: values.cors,
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAaxC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AA0ED,wBAAsB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAaxC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AA0ED,wBAAsB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAuF7E;AAYD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAqC3E;AAqBD,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,SAAS,CAAC,CAQpB;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,EACnB,gBAAgB,EAAE,MAAM,SAAS,EACjC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GACpC,WAAW,CA2Bb;AAED,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDvD"}
|
package/dist/cli.js
CHANGED
|
@@ -92,12 +92,22 @@ export async function createCliServer(options) {
|
|
|
92
92
|
const method = toHttpMethod(req.method ?? "GET");
|
|
93
93
|
const headers = parseNodeHeaders(req);
|
|
94
94
|
const query = parseNodeQuery(url);
|
|
95
|
-
void collectBody(req, headers)
|
|
95
|
+
void collectBody(req, headers)
|
|
96
|
+
.then((body) => mock
|
|
96
97
|
.handle(method, path, { headers, body, query })
|
|
97
98
|
.then((schmockResponse) => {
|
|
98
99
|
const extra = cors ? CORS_HEADERS : undefined;
|
|
99
100
|
writeSchmockResponse(res, schmockResponse, extra);
|
|
100
|
-
}))
|
|
101
|
+
}))
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
if (!res.headersSent) {
|
|
104
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
105
|
+
}
|
|
106
|
+
res.end(JSON.stringify({
|
|
107
|
+
error: error instanceof Error ? error.message : "Internal Server Error",
|
|
108
|
+
code: "SERVER_ERROR",
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
101
111
|
});
|
|
102
112
|
return new Promise((resolve, reject) => {
|
|
103
113
|
httpServer.on("error", reject);
|
|
@@ -115,6 +125,13 @@ export async function createCliServer(options) {
|
|
|
115
125
|
});
|
|
116
126
|
});
|
|
117
127
|
}
|
|
128
|
+
function validatePort(value) {
|
|
129
|
+
const port = Number(value);
|
|
130
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
131
|
+
throw new Error(`Invalid port "${value}". Port must be an integer between 0 and 65535.`);
|
|
132
|
+
}
|
|
133
|
+
return port;
|
|
134
|
+
}
|
|
118
135
|
export function parseCliArgs(args) {
|
|
119
136
|
const { values, positionals } = parseArgs({
|
|
120
137
|
args,
|
|
@@ -137,7 +154,7 @@ export function parseCliArgs(args) {
|
|
|
137
154
|
const spec = values.spec ?? positionals[0] ?? "";
|
|
138
155
|
return {
|
|
139
156
|
spec,
|
|
140
|
-
port: values.port ?
|
|
157
|
+
port: values.port ? validatePort(values.port) : undefined,
|
|
141
158
|
hostname: values.hostname,
|
|
142
159
|
seed: values.seed,
|
|
143
160
|
cors: values.cors,
|
package/dist/index.js
CHANGED
|
@@ -17734,6 +17734,7 @@ import { parseArgs } from "node:util";
|
|
|
17734
17734
|
import { createServer } from "node:http";
|
|
17735
17735
|
|
|
17736
17736
|
// ../core/src/constants.ts
|
|
17737
|
+
var ROUTE_NOT_FOUND_CODE = "ROUTE_NOT_FOUND";
|
|
17737
17738
|
var HTTP_METHODS = [
|
|
17738
17739
|
"GET",
|
|
17739
17740
|
"POST",
|
|
@@ -17760,6 +17761,10 @@ function toRouteKey(method, path) {
|
|
|
17760
17761
|
const key = `${method} ${path}`;
|
|
17761
17762
|
return key;
|
|
17762
17763
|
}
|
|
17764
|
+
function isRouteNotFound(response) {
|
|
17765
|
+
const { status, body } = response;
|
|
17766
|
+
return status === 404 && body !== null && typeof body === "object" && "code" in body && body.code === ROUTE_NOT_FOUND_CODE;
|
|
17767
|
+
}
|
|
17763
17768
|
function isStatusTuple(value) {
|
|
17764
17769
|
return Array.isArray(value) && (value.length === 2 || value.length === 3) && typeof value[0] === "number" && value[0] >= 100 && value[0] <= 599;
|
|
17765
17770
|
}
|
|
@@ -17893,6 +17898,177 @@ function writeSchmockResponse(res, response, extraHeaders) {
|
|
|
17893
17898
|
res.end(responseBody);
|
|
17894
17899
|
}
|
|
17895
17900
|
|
|
17901
|
+
// ../core/src/interceptor.ts
|
|
17902
|
+
function extractPathname(url) {
|
|
17903
|
+
const queryStart = url.indexOf("?");
|
|
17904
|
+
const urlWithoutQuery = queryStart === -1 ? url : url.slice(0, queryStart);
|
|
17905
|
+
if (urlWithoutQuery.includes("://")) {
|
|
17906
|
+
try {
|
|
17907
|
+
return new URL(urlWithoutQuery).pathname;
|
|
17908
|
+
} catch {}
|
|
17909
|
+
}
|
|
17910
|
+
if (!urlWithoutQuery.startsWith("/")) {
|
|
17911
|
+
return `/${urlWithoutQuery}`;
|
|
17912
|
+
}
|
|
17913
|
+
return urlWithoutQuery;
|
|
17914
|
+
}
|
|
17915
|
+
function extractQuery(url) {
|
|
17916
|
+
const queryStart = url.indexOf("?");
|
|
17917
|
+
if (queryStart === -1)
|
|
17918
|
+
return {};
|
|
17919
|
+
const params = new URLSearchParams(url.slice(queryStart + 1));
|
|
17920
|
+
const result = {};
|
|
17921
|
+
params.forEach((value, key) => {
|
|
17922
|
+
result[key] = value;
|
|
17923
|
+
});
|
|
17924
|
+
return result;
|
|
17925
|
+
}
|
|
17926
|
+
function extractHeaders(input, init) {
|
|
17927
|
+
const headers = {};
|
|
17928
|
+
const raw = init?.headers ?? (input instanceof Request ? input.headers : undefined);
|
|
17929
|
+
if (!raw)
|
|
17930
|
+
return headers;
|
|
17931
|
+
if (raw instanceof Headers) {
|
|
17932
|
+
raw.forEach((value, key) => {
|
|
17933
|
+
headers[key] = value;
|
|
17934
|
+
});
|
|
17935
|
+
} else if (Array.isArray(raw)) {
|
|
17936
|
+
for (const [key, value] of raw) {
|
|
17937
|
+
headers[key.toLowerCase()] = value;
|
|
17938
|
+
}
|
|
17939
|
+
} else {
|
|
17940
|
+
for (const key of Object.keys(raw)) {
|
|
17941
|
+
headers[key.toLowerCase()] = raw[key];
|
|
17942
|
+
}
|
|
17943
|
+
}
|
|
17944
|
+
return headers;
|
|
17945
|
+
}
|
|
17946
|
+
async function extractBody(input, init) {
|
|
17947
|
+
const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
|
|
17948
|
+
if (bodyInit === null || bodyInit === undefined)
|
|
17949
|
+
return;
|
|
17950
|
+
if (typeof bodyInit === "string") {
|
|
17951
|
+
try {
|
|
17952
|
+
return JSON.parse(bodyInit);
|
|
17953
|
+
} catch {
|
|
17954
|
+
return bodyInit;
|
|
17955
|
+
}
|
|
17956
|
+
}
|
|
17957
|
+
if (bodyInit instanceof URLSearchParams) {
|
|
17958
|
+
const result = {};
|
|
17959
|
+
bodyInit.forEach((value, key) => {
|
|
17960
|
+
result[key] = value;
|
|
17961
|
+
});
|
|
17962
|
+
return result;
|
|
17963
|
+
}
|
|
17964
|
+
if (input instanceof Request && !init?.body && input.body) {
|
|
17965
|
+
try {
|
|
17966
|
+
return await input.clone().json();
|
|
17967
|
+
} catch {
|
|
17968
|
+
try {
|
|
17969
|
+
return await input.clone().text();
|
|
17970
|
+
} catch {
|
|
17971
|
+
return;
|
|
17972
|
+
}
|
|
17973
|
+
}
|
|
17974
|
+
}
|
|
17975
|
+
return;
|
|
17976
|
+
}
|
|
17977
|
+
function createFetchInterceptor(handle, options = {}) {
|
|
17978
|
+
const {
|
|
17979
|
+
baseUrl,
|
|
17980
|
+
passthrough = true,
|
|
17981
|
+
beforeRequest,
|
|
17982
|
+
beforeResponse,
|
|
17983
|
+
errorFormatter
|
|
17984
|
+
} = options;
|
|
17985
|
+
const originalFetch = globalThis.fetch;
|
|
17986
|
+
let active = true;
|
|
17987
|
+
globalThis.fetch = async (input, init) => {
|
|
17988
|
+
const urlString = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
|
|
17989
|
+
const path = extractPathname(urlString);
|
|
17990
|
+
if (baseUrl) {
|
|
17991
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
17992
|
+
const isMatch = path === normalizedBase || path.startsWith(`${normalizedBase}/`);
|
|
17993
|
+
if (!isMatch) {
|
|
17994
|
+
return originalFetch(input, init);
|
|
17995
|
+
}
|
|
17996
|
+
}
|
|
17997
|
+
const method = input instanceof Request ? input.method : init?.method ?? "GET";
|
|
17998
|
+
const headers = extractHeaders(input, init);
|
|
17999
|
+
const query = extractQuery(urlString);
|
|
18000
|
+
const body = await extractBody(input, init);
|
|
18001
|
+
let adapterRequest = {
|
|
18002
|
+
method,
|
|
18003
|
+
path,
|
|
18004
|
+
headers,
|
|
18005
|
+
body,
|
|
18006
|
+
query
|
|
18007
|
+
};
|
|
18008
|
+
try {
|
|
18009
|
+
if (beforeRequest) {
|
|
18010
|
+
const modified = await beforeRequest(adapterRequest);
|
|
18011
|
+
if (modified) {
|
|
18012
|
+
adapterRequest = modified;
|
|
18013
|
+
}
|
|
18014
|
+
}
|
|
18015
|
+
const schmockResponse = await handle(toHttpMethod(adapterRequest.method), adapterRequest.path, {
|
|
18016
|
+
headers: adapterRequest.headers,
|
|
18017
|
+
body: adapterRequest.body,
|
|
18018
|
+
query: adapterRequest.query
|
|
18019
|
+
});
|
|
18020
|
+
if (isRouteNotFound(schmockResponse)) {
|
|
18021
|
+
if (passthrough) {
|
|
18022
|
+
return originalFetch(input, init);
|
|
18023
|
+
}
|
|
18024
|
+
return new Response(JSON.stringify({
|
|
18025
|
+
error: "No matching mock route found",
|
|
18026
|
+
code: "ROUTE_NOT_FOUND"
|
|
18027
|
+
}), {
|
|
18028
|
+
status: 404,
|
|
18029
|
+
headers: { "content-type": "application/json" }
|
|
18030
|
+
});
|
|
18031
|
+
}
|
|
18032
|
+
let response = schmockResponse;
|
|
18033
|
+
if (beforeResponse) {
|
|
18034
|
+
const modified = await beforeResponse(response, adapterRequest);
|
|
18035
|
+
if (modified) {
|
|
18036
|
+
response = modified;
|
|
18037
|
+
}
|
|
18038
|
+
}
|
|
18039
|
+
const responseHeaders = new Headers(response.headers);
|
|
18040
|
+
if (!responseHeaders.has("content-type") && response.body !== null && response.body !== undefined) {
|
|
18041
|
+
responseHeaders.set("content-type", "application/json");
|
|
18042
|
+
}
|
|
18043
|
+
const responseBody = response.body === null || response.body === undefined ? null : typeof response.body === "string" ? response.body : JSON.stringify(response.body);
|
|
18044
|
+
return new Response(responseBody, {
|
|
18045
|
+
status: response.status,
|
|
18046
|
+
headers: responseHeaders
|
|
18047
|
+
});
|
|
18048
|
+
} catch (error) {
|
|
18049
|
+
if (errorFormatter) {
|
|
18050
|
+
const formatted = errorFormatter(error instanceof Error ? error : new Error(String(error)));
|
|
18051
|
+
return new Response(JSON.stringify(formatted), {
|
|
18052
|
+
status: 500,
|
|
18053
|
+
headers: { "content-type": "application/json" }
|
|
18054
|
+
});
|
|
18055
|
+
}
|
|
18056
|
+
throw error;
|
|
18057
|
+
}
|
|
18058
|
+
};
|
|
18059
|
+
return {
|
|
18060
|
+
restore() {
|
|
18061
|
+
if (active) {
|
|
18062
|
+
globalThis.fetch = originalFetch;
|
|
18063
|
+
active = false;
|
|
18064
|
+
}
|
|
18065
|
+
},
|
|
18066
|
+
get active() {
|
|
18067
|
+
return active;
|
|
18068
|
+
}
|
|
18069
|
+
};
|
|
18070
|
+
}
|
|
18071
|
+
|
|
17896
18072
|
// ../core/src/parser.ts
|
|
17897
18073
|
function parseRouteKey(routeKey) {
|
|
17898
18074
|
const match = routeKey.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (.+)$/);
|
|
@@ -17927,7 +18103,7 @@ async function runPluginPipeline(plugins, context, initialResponse, logger) {
|
|
|
17927
18103
|
logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
17928
18104
|
try {
|
|
17929
18105
|
const result = await plugin.process(currentContext, response);
|
|
17930
|
-
if (!result
|
|
18106
|
+
if (!result?.context) {
|
|
17931
18107
|
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
17932
18108
|
}
|
|
17933
18109
|
currentContext = result.context;
|
|
@@ -18077,6 +18253,7 @@ class CallableMockInstance {
|
|
|
18077
18253
|
callableRef;
|
|
18078
18254
|
server;
|
|
18079
18255
|
serverInfo;
|
|
18256
|
+
interceptHandle = null;
|
|
18080
18257
|
listeners = new Map;
|
|
18081
18258
|
constructor(globalConfig = {}) {
|
|
18082
18259
|
this.globalConfig = globalConfig;
|
|
@@ -18209,6 +18386,8 @@ class CallableMockInstance {
|
|
|
18209
18386
|
}
|
|
18210
18387
|
}
|
|
18211
18388
|
reset() {
|
|
18389
|
+
this.interceptHandle?.restore();
|
|
18390
|
+
this.interceptHandle = null;
|
|
18212
18391
|
this.close();
|
|
18213
18392
|
this.routes = [];
|
|
18214
18393
|
this.staticRoutes.clear();
|
|
@@ -18279,11 +18458,19 @@ class CallableMockInstance {
|
|
|
18279
18458
|
if (!this.server) {
|
|
18280
18459
|
return;
|
|
18281
18460
|
}
|
|
18461
|
+
this.server.closeAllConnections();
|
|
18282
18462
|
this.server.close();
|
|
18283
18463
|
this.server = undefined;
|
|
18284
18464
|
this.serverInfo = undefined;
|
|
18285
18465
|
this.logger.log("server", "Server stopped");
|
|
18286
18466
|
}
|
|
18467
|
+
intercept(options) {
|
|
18468
|
+
if (this.interceptHandle?.active) {
|
|
18469
|
+
throw new SchmockError("Already intercepting. Call restore() first.", "ALREADY_INTERCEPTING");
|
|
18470
|
+
}
|
|
18471
|
+
this.interceptHandle = createFetchInterceptor((method, path, opts) => this.handle(method, path, opts), options);
|
|
18472
|
+
return this.interceptHandle;
|
|
18473
|
+
}
|
|
18287
18474
|
async handle(method, path, options) {
|
|
18288
18475
|
const handleStart = performance.now();
|
|
18289
18476
|
const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
|
|
@@ -18474,13 +18661,14 @@ function schmock(config) {
|
|
|
18474
18661
|
getRoutes: instance.getRoutes.bind(instance),
|
|
18475
18662
|
getState: instance.getState.bind(instance),
|
|
18476
18663
|
listen: instance.listen.bind(instance),
|
|
18477
|
-
close: instance.close.bind(instance)
|
|
18664
|
+
close: instance.close.bind(instance),
|
|
18665
|
+
intercept: (options) => instance.intercept(options)
|
|
18478
18666
|
});
|
|
18479
18667
|
instance.setCallableRef(callableInstance);
|
|
18480
18668
|
return callableInstance;
|
|
18481
18669
|
}
|
|
18482
18670
|
// ../openapi/package.json
|
|
18483
|
-
var version = "
|
|
18671
|
+
var version = "2.0.1";
|
|
18484
18672
|
|
|
18485
18673
|
// ../openapi/src/utils.ts
|
|
18486
18674
|
function isRecord(value) {
|
|
@@ -18498,14 +18686,16 @@ function getRouteCallbacks(route) {
|
|
|
18498
18686
|
function fireCallbacks(callbacks, context, response) {
|
|
18499
18687
|
for (const callback of callbacks) {
|
|
18500
18688
|
const url = resolveCallbackUrl(callback.urlExpression, context, response);
|
|
18501
|
-
if (!url
|
|
18689
|
+
if (!url?.startsWith("http"))
|
|
18502
18690
|
continue;
|
|
18503
18691
|
const body = Array.isArray(response) ? response[1] : response;
|
|
18504
18692
|
fetch(url, {
|
|
18505
18693
|
method: callback.method,
|
|
18506
18694
|
headers: { "content-type": "application/json" },
|
|
18507
18695
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
18508
|
-
}).catch(() => {
|
|
18696
|
+
}).catch((error) => {
|
|
18697
|
+
console.warn(`[@schmock/openapi] Callback ${callback.method} ${url} failed:`, error instanceof Error ? error.message : error);
|
|
18698
|
+
});
|
|
18509
18699
|
}
|
|
18510
18700
|
}
|
|
18511
18701
|
function resolveCallbackUrl(expression, context, response) {
|
|
@@ -18551,7 +18741,6 @@ function resolveJsonPointer(obj, pointer) {
|
|
|
18551
18741
|
|
|
18552
18742
|
// ../openapi/src/generators.ts
|
|
18553
18743
|
import { randomUUID } from "node:crypto";
|
|
18554
|
-
|
|
18555
18744
|
// ../faker/src/constants.ts
|
|
18556
18745
|
var MAX_ARRAY_SIZE = 1e4;
|
|
18557
18746
|
var MAX_NESTING_DEPTH = 10;
|
|
@@ -22436,7 +22625,9 @@ function determineArrayCount(schema, explicitCount, random = Math.random) {
|
|
|
22436
22625
|
return explicitCount;
|
|
22437
22626
|
}
|
|
22438
22627
|
if (schema.minItems !== undefined && schema.maxItems !== undefined) {
|
|
22439
|
-
|
|
22628
|
+
const min = schema.minItems;
|
|
22629
|
+
const max = Math.max(min, schema.maxItems);
|
|
22630
|
+
return Math.floor(random() * (max - min + 1)) + min;
|
|
22440
22631
|
}
|
|
22441
22632
|
if (schema.minItems !== undefined) {
|
|
22442
22633
|
return Math.max(schema.minItems, DEFAULT_ARRAY_COUNT);
|
|
@@ -22449,6 +22640,8 @@ function determineArrayCount(schema, explicitCount, random = Math.random) {
|
|
|
22449
22640
|
function applyOverrides(data, overrides, params, state, query) {
|
|
22450
22641
|
if (!overrides)
|
|
22451
22642
|
return data;
|
|
22643
|
+
if (typeof data !== "object" || data === null)
|
|
22644
|
+
return data;
|
|
22452
22645
|
const result = structuredClone(data);
|
|
22453
22646
|
for (const [key, value] of Object.entries(overrides)) {
|
|
22454
22647
|
if (DANGEROUS_KEYS.has(key))
|
|
@@ -22457,10 +22650,11 @@ function applyOverrides(data, overrides, params, state, query) {
|
|
|
22457
22650
|
setNestedProperty(result, key, value, { params, state, query });
|
|
22458
22651
|
} else {
|
|
22459
22652
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
22653
|
+
const nested = value;
|
|
22460
22654
|
if (result[key] && typeof result[key] === "object") {
|
|
22461
|
-
result[key] = applyOverrides(result[key],
|
|
22655
|
+
result[key] = applyOverrides(result[key], nested, params, state, query);
|
|
22462
22656
|
} else {
|
|
22463
|
-
result[key] = applyOverrides({},
|
|
22657
|
+
result[key] = applyOverrides({}, nested, params, state, query);
|
|
22464
22658
|
}
|
|
22465
22659
|
} else if (typeof value === "string" && value.includes("{{")) {
|
|
22466
22660
|
result[key] = processTemplate(value, { params, state, query });
|
|
@@ -22492,34 +22686,28 @@ function setNestedProperty(obj, path, value, context) {
|
|
|
22492
22686
|
current[finalKey] = value;
|
|
22493
22687
|
}
|
|
22494
22688
|
}
|
|
22689
|
+
function resolveTemplatePath(context, expression) {
|
|
22690
|
+
const parts = expression.trim().split(".");
|
|
22691
|
+
let result = context;
|
|
22692
|
+
for (const part of parts) {
|
|
22693
|
+
if (result && typeof result === "object") {
|
|
22694
|
+
result = result[part];
|
|
22695
|
+
} else {
|
|
22696
|
+
return;
|
|
22697
|
+
}
|
|
22698
|
+
}
|
|
22699
|
+
return result;
|
|
22700
|
+
}
|
|
22495
22701
|
function processTemplate(template, context) {
|
|
22496
22702
|
const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
|
|
22497
22703
|
if (singleTemplateMatch) {
|
|
22498
|
-
const
|
|
22499
|
-
const parts = expression.trim().split(".");
|
|
22500
|
-
let result = context;
|
|
22501
|
-
for (const part of parts) {
|
|
22502
|
-
if (result && typeof result === "object") {
|
|
22503
|
-
result = result[part];
|
|
22504
|
-
} else {
|
|
22505
|
-
return template;
|
|
22506
|
-
}
|
|
22507
|
-
}
|
|
22704
|
+
const result = resolveTemplatePath(context, singleTemplateMatch[1]);
|
|
22508
22705
|
return result !== undefined ? result : template;
|
|
22509
22706
|
}
|
|
22510
|
-
|
|
22511
|
-
const
|
|
22512
|
-
let result = context;
|
|
22513
|
-
for (const part of parts) {
|
|
22514
|
-
if (result && typeof result === "object") {
|
|
22515
|
-
result = result[part];
|
|
22516
|
-
} else {
|
|
22517
|
-
return match;
|
|
22518
|
-
}
|
|
22519
|
-
}
|
|
22707
|
+
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expression) => {
|
|
22708
|
+
const result = resolveTemplatePath(context, expression);
|
|
22520
22709
|
return result !== undefined ? String(result) : match;
|
|
22521
22710
|
});
|
|
22522
|
-
return processed;
|
|
22523
22711
|
}
|
|
22524
22712
|
|
|
22525
22713
|
// ../faker/src/field-mappings.ts
|
|
@@ -23991,7 +24179,6 @@ function enhanceFieldSchema(fieldName, fieldSchema) {
|
|
|
23991
24179
|
async function generateFromSchema(options) {
|
|
23992
24180
|
const { schema, count, overrides, params, state, query, seed } = options;
|
|
23993
24181
|
validateSchema(schema);
|
|
23994
|
-
let generated;
|
|
23995
24182
|
if (schema.type === "array" && schema.items) {
|
|
23996
24183
|
const itemCount = determineArrayCount(schema, count);
|
|
23997
24184
|
if (itemCount > MAX_ARRAY_SIZE) {
|
|
@@ -24003,38 +24190,39 @@ async function generateFromSchema(options) {
|
|
|
24003
24190
|
}
|
|
24004
24191
|
const itemSchema = rawItemSchema;
|
|
24005
24192
|
const enhancedItemSchema = enhanceSchemaWithSmartMapping(itemSchema);
|
|
24006
|
-
|
|
24193
|
+
const items = [];
|
|
24007
24194
|
for (let i3 = 0;i3 < itemCount; i3++) {
|
|
24008
24195
|
let item = await generateWithJsf(enhancedItemSchema, seed);
|
|
24009
24196
|
item = postProcessGenerated(item, enhancedItemSchema);
|
|
24010
24197
|
item = applyOverrides(item, overrides, params, state, query);
|
|
24011
|
-
|
|
24198
|
+
items.push(item);
|
|
24012
24199
|
}
|
|
24013
|
-
|
|
24014
|
-
const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
|
|
24015
|
-
generated = await generateWithJsf(enhancedSchema, seed);
|
|
24016
|
-
generated = postProcessGenerated(generated, enhancedSchema);
|
|
24017
|
-
generated = applyOverrides(generated, overrides, params, state, query);
|
|
24200
|
+
return items;
|
|
24018
24201
|
}
|
|
24202
|
+
const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
|
|
24203
|
+
let generated = await generateWithJsf(enhancedSchema, seed);
|
|
24204
|
+
generated = postProcessGenerated(generated, enhancedSchema);
|
|
24205
|
+
generated = applyOverrides(generated, overrides, params, state, query);
|
|
24019
24206
|
return generated;
|
|
24020
24207
|
}
|
|
24021
24208
|
function postProcessGenerated(data, schema) {
|
|
24022
24209
|
if (data === null || data === undefined || !schema || typeof schema !== "object") {
|
|
24023
24210
|
return data;
|
|
24024
24211
|
}
|
|
24025
|
-
const
|
|
24026
|
-
if (
|
|
24212
|
+
const schemaExt = schema;
|
|
24213
|
+
if (schemaExt.schmockNullable === true) {
|
|
24027
24214
|
if (Math.random() < NULLABLE_NULL_PROBABILITY) {
|
|
24028
24215
|
return null;
|
|
24029
24216
|
}
|
|
24030
24217
|
}
|
|
24031
|
-
if (schema.type === "boolean" && typeof
|
|
24032
|
-
return Math.random() <
|
|
24218
|
+
if (schema.type === "boolean" && typeof schemaExt.schmockTrueProbability === "number") {
|
|
24219
|
+
return Math.random() < schemaExt.schmockTrueProbability;
|
|
24033
24220
|
}
|
|
24034
24221
|
if (typeof data === "object" && !Array.isArray(data) && schema.properties) {
|
|
24222
|
+
const record = data;
|
|
24035
24223
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
24036
|
-
if (key in
|
|
24037
|
-
|
|
24224
|
+
if (key in record && isJSONSchema7(propSchema)) {
|
|
24225
|
+
record[key] = postProcessGenerated(record[key], propSchema);
|
|
24038
24226
|
}
|
|
24039
24227
|
}
|
|
24040
24228
|
}
|
|
@@ -24295,7 +24483,9 @@ async function generateErrorResponse(status, meta) {
|
|
|
24295
24483
|
try {
|
|
24296
24484
|
const body = await generateFromSchema({ schema: errorSchema });
|
|
24297
24485
|
return toTuple(status, body);
|
|
24298
|
-
} catch {
|
|
24486
|
+
} catch (error) {
|
|
24487
|
+
console.warn(`[@schmock/openapi] Error schema generation failed for status ${status}:`, error instanceof Error ? error.message : error);
|
|
24488
|
+
}
|
|
24299
24489
|
}
|
|
24300
24490
|
const defaults = {
|
|
24301
24491
|
404: { error: "Not found", code: "NOT_FOUND" },
|
|
@@ -24307,7 +24497,8 @@ async function generateErrorResponse(status, meta) {
|
|
|
24307
24497
|
async function generateWrapperSkeleton(schema) {
|
|
24308
24498
|
try {
|
|
24309
24499
|
return await generateFromSchema({ schema });
|
|
24310
|
-
} catch {
|
|
24500
|
+
} catch (error) {
|
|
24501
|
+
console.warn("[@schmock/openapi] Wrapper skeleton generation failed:", error instanceof Error ? error.message : error);
|
|
24311
24502
|
return {};
|
|
24312
24503
|
}
|
|
24313
24504
|
}
|
|
@@ -24886,7 +25077,7 @@ async function parseSpec(source) {
|
|
|
24886
25077
|
}
|
|
24887
25078
|
const isSwagger2 = "swagger" in api && typeof api.swagger === "string";
|
|
24888
25079
|
const title = api.info?.title ?? "Untitled";
|
|
24889
|
-
const
|
|
25080
|
+
const version3 = api.info?.version ?? "0.0.0";
|
|
24890
25081
|
let basePath = "";
|
|
24891
25082
|
if (isSwagger2 && "basePath" in api) {
|
|
24892
25083
|
const bp = api.basePath;
|
|
@@ -24911,7 +25102,7 @@ async function parseSpec(source) {
|
|
|
24911
25102
|
const paths = [];
|
|
24912
25103
|
const rawPaths = "paths" in api && isRecord(api.paths) ? api.paths : undefined;
|
|
24913
25104
|
if (!rawPaths) {
|
|
24914
|
-
return { title, version:
|
|
25105
|
+
return { title, version: version3, basePath, paths, securitySchemes, globalSecurity };
|
|
24915
25106
|
}
|
|
24916
25107
|
for (const [pathTemplate, pathItemRaw] of Object.entries(rawPaths)) {
|
|
24917
25108
|
if (!isRecord(pathItemRaw))
|
|
@@ -24952,7 +25143,7 @@ async function parseSpec(source) {
|
|
|
24952
25143
|
});
|
|
24953
25144
|
}
|
|
24954
25145
|
}
|
|
24955
|
-
return { title, version:
|
|
25146
|
+
return { title, version: version3, basePath, paths, securitySchemes, globalSecurity };
|
|
24956
25147
|
}
|
|
24957
25148
|
function isValidParamLocation(location2, isSwagger2) {
|
|
24958
25149
|
const validLocations = isSwagger2 ? ["path", "query", "header", "body"] : ["path", "query", "header"];
|
|
@@ -25483,7 +25674,8 @@ async function generateResponseBody(schema, seed, onSchema, context) {
|
|
|
25483
25674
|
}
|
|
25484
25675
|
try {
|
|
25485
25676
|
return await generateFromSchema({ schema: finalSchema, seed });
|
|
25486
|
-
} catch {
|
|
25677
|
+
} catch (error) {
|
|
25678
|
+
console.warn("[@schmock/openapi] Response body generation failed:", error instanceof Error ? error.message : error);
|
|
25487
25679
|
return {};
|
|
25488
25680
|
}
|
|
25489
25681
|
}
|
|
@@ -25697,7 +25889,15 @@ async function createCliServer(options) {
|
|
|
25697
25889
|
collectBody(req, headers).then((body) => mock.handle(method, path, { headers, body, query }).then((schmockResponse) => {
|
|
25698
25890
|
const extra = cors ? CORS_HEADERS : undefined;
|
|
25699
25891
|
writeSchmockResponse(res, schmockResponse, extra);
|
|
25700
|
-
}))
|
|
25892
|
+
})).catch((error) => {
|
|
25893
|
+
if (!res.headersSent) {
|
|
25894
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
25895
|
+
}
|
|
25896
|
+
res.end(JSON.stringify({
|
|
25897
|
+
error: error instanceof Error ? error.message : "Internal Server Error",
|
|
25898
|
+
code: "SERVER_ERROR"
|
|
25899
|
+
}));
|
|
25900
|
+
});
|
|
25701
25901
|
});
|
|
25702
25902
|
return new Promise((resolve, reject) => {
|
|
25703
25903
|
httpServer.on("error", reject);
|
|
@@ -25715,6 +25915,13 @@ async function createCliServer(options) {
|
|
|
25715
25915
|
});
|
|
25716
25916
|
});
|
|
25717
25917
|
}
|
|
25918
|
+
function validatePort(value) {
|
|
25919
|
+
const port = Number(value);
|
|
25920
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
25921
|
+
throw new Error(`Invalid port "${value}". Port must be an integer between 0 and 65535.`);
|
|
25922
|
+
}
|
|
25923
|
+
return port;
|
|
25924
|
+
}
|
|
25718
25925
|
function parseCliArgs(args) {
|
|
25719
25926
|
const { values, positionals } = parseArgs({
|
|
25720
25927
|
args,
|
|
@@ -25737,7 +25944,7 @@ function parseCliArgs(args) {
|
|
|
25737
25944
|
const spec = values.spec ?? positionals[0] ?? "";
|
|
25738
25945
|
return {
|
|
25739
25946
|
spec,
|
|
25740
|
-
port: values.port ?
|
|
25947
|
+
port: values.port ? validatePort(values.port) : undefined,
|
|
25741
25948
|
hostname: values.hostname,
|
|
25742
25949
|
seed: values.seed,
|
|
25743
25950
|
cors: values.cors,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/cli",
|
|
3
3
|
"description": "CLI for running Schmock mock servers from OpenAPI specs",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@schmock/core": "^
|
|
36
|
-
"@schmock/openapi": "^
|
|
35
|
+
"@schmock/core": "^2.0.0",
|
|
36
|
+
"@schmock/openapi": "^2.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@amiceli/vitest-cucumber": "^6.3.0",
|