@apicircle/mock-server-core 1.0.9 → 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/dist/index.cjs +235 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +234 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
MockServerStartError: () => MockServerStartError,
|
|
33
34
|
buildRouter: () => buildRouter,
|
|
34
35
|
createMockApp: () => createMockApp,
|
|
35
36
|
getFreePort: () => getFreePort,
|
|
@@ -48,6 +49,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
48
49
|
// src/parsers/openapi.ts
|
|
49
50
|
var import_swagger_parser = __toESM(require("@apidevtools/swagger-parser"), 1);
|
|
50
51
|
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
52
|
+
var import_shared2 = require("@apicircle/shared");
|
|
51
53
|
|
|
52
54
|
// src/faker/schemaToExample.ts
|
|
53
55
|
var FORMAT_DEFAULTS = {
|
|
@@ -110,6 +112,17 @@ function pickType(type) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// src/parsers/buildEndpoint.ts
|
|
115
|
+
var import_shared = require("@apicircle/shared");
|
|
116
|
+
function paramDef(name, opts) {
|
|
117
|
+
return {
|
|
118
|
+
id: (0, import_shared.generateId)(),
|
|
119
|
+
name,
|
|
120
|
+
typeHint: opts?.typeHint,
|
|
121
|
+
required: opts?.required,
|
|
122
|
+
description: opts?.description,
|
|
123
|
+
example: opts?.example
|
|
124
|
+
};
|
|
125
|
+
}
|
|
113
126
|
function bodyTypeForContentType(contentType) {
|
|
114
127
|
if (!contentType) return "json";
|
|
115
128
|
const main = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
@@ -150,7 +163,7 @@ function buildMockEndpoint(input) {
|
|
|
150
163
|
method: input.method,
|
|
151
164
|
pathPattern: input.pathPattern,
|
|
152
165
|
description: input.description,
|
|
153
|
-
requestSchema:
|
|
166
|
+
requestSchema: input.requestSchema ?? (0, import_shared.makeDefaultRequestSchema)(),
|
|
154
167
|
requestValidation: [],
|
|
155
168
|
responseRules: [],
|
|
156
169
|
defaultResponse: buildMockResponse(input.response),
|
|
@@ -189,18 +202,64 @@ async function parseOpenApiToEndpoints(source, format = "json", opts = {}) {
|
|
|
189
202
|
let endpointId = 0;
|
|
190
203
|
for (const [path, ops] of Object.entries(paths)) {
|
|
191
204
|
if (!ops || typeof ops !== "object") continue;
|
|
205
|
+
const pathItemParams = ops.parameters ?? [];
|
|
192
206
|
for (const method of Object.keys(ops)) {
|
|
193
207
|
const upper = method.toUpperCase();
|
|
194
208
|
if (!SUPPORTED_METHODS.includes(upper)) continue;
|
|
195
209
|
const op = ops[method];
|
|
196
210
|
if (!op || typeof op !== "object") continue;
|
|
197
|
-
const built = buildEndpointFromOp(
|
|
211
|
+
const built = buildEndpointFromOp(
|
|
212
|
+
path,
|
|
213
|
+
upper,
|
|
214
|
+
op,
|
|
215
|
+
pathItemParams,
|
|
216
|
+
opts,
|
|
217
|
+
warnings,
|
|
218
|
+
endpointId++
|
|
219
|
+
);
|
|
198
220
|
if (built) endpoints.push(built);
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
return { endpoints, warnings };
|
|
202
224
|
}
|
|
203
|
-
function
|
|
225
|
+
function buildRequestSchema(pathItemParams, op) {
|
|
226
|
+
const merged = /* @__PURE__ */ new Map();
|
|
227
|
+
for (const p of [...pathItemParams, ...op.parameters ?? []]) {
|
|
228
|
+
if (p && typeof p === "object" && typeof p.name === "string") {
|
|
229
|
+
merged.set(`${p.in ?? "query"}:${p.name}`, p);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const schema = (0, import_shared2.makeDefaultRequestSchema)();
|
|
233
|
+
for (const p of merged.values()) {
|
|
234
|
+
const rawType = p.schema?.type ?? p.type;
|
|
235
|
+
const typeHint = p.schema?.format ?? (Array.isArray(rawType) ? rawType[0] : rawType);
|
|
236
|
+
const exampleVal = p.example ?? p.schema?.example;
|
|
237
|
+
const def = paramDef(p.name, {
|
|
238
|
+
typeHint: typeof typeHint === "string" ? typeHint : void 0,
|
|
239
|
+
required: typeof p.required === "boolean" ? p.required : void 0,
|
|
240
|
+
description: typeof p.description === "string" ? p.description : void 0,
|
|
241
|
+
example: exampleVal === void 0 ? void 0 : typeof exampleVal === "string" ? exampleVal : JSON.stringify(exampleVal)
|
|
242
|
+
});
|
|
243
|
+
switch (p.in) {
|
|
244
|
+
case "path":
|
|
245
|
+
schema.pathParams.push(def);
|
|
246
|
+
break;
|
|
247
|
+
case "query":
|
|
248
|
+
schema.queryParams.push(def);
|
|
249
|
+
break;
|
|
250
|
+
case "header":
|
|
251
|
+
schema.headers.push(def);
|
|
252
|
+
break;
|
|
253
|
+
case "cookie":
|
|
254
|
+
schema.cookies.push(def);
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return schema;
|
|
261
|
+
}
|
|
262
|
+
function buildEndpointFromOp(path, method, op, pathItemParams, opts, warnings, index) {
|
|
204
263
|
const responses = op.responses ?? {};
|
|
205
264
|
const candidates = Object.keys(responses).filter((code) => /^2\d\d$/.test(code)).map((code) => Number(code));
|
|
206
265
|
if (candidates.length === 0) {
|
|
@@ -220,6 +279,7 @@ function buildEndpointFromOp(path, method, op, opts, warnings, index) {
|
|
|
220
279
|
method,
|
|
221
280
|
pathPattern: path,
|
|
222
281
|
example: exampleName,
|
|
282
|
+
requestSchema: buildRequestSchema(pathItemParams, op),
|
|
223
283
|
response: { status, headers, body }
|
|
224
284
|
});
|
|
225
285
|
}
|
|
@@ -322,6 +382,26 @@ function safeYamlLoad(s) {
|
|
|
322
382
|
}
|
|
323
383
|
|
|
324
384
|
// src/parsers/postman.ts
|
|
385
|
+
var import_shared3 = require("@apicircle/shared");
|
|
386
|
+
function postmanRequestSchema(req) {
|
|
387
|
+
const schema = (0, import_shared3.makeDefaultRequestSchema)();
|
|
388
|
+
const url = req.url;
|
|
389
|
+
if (url && typeof url === "object") {
|
|
390
|
+
for (const v of url.variable ?? []) {
|
|
391
|
+
if (v?.key)
|
|
392
|
+
schema.pathParams.push(paramDef(v.key, { example: v.value, description: v.description }));
|
|
393
|
+
}
|
|
394
|
+
for (const q of url.query ?? []) {
|
|
395
|
+
if (q?.key && !q.disabled)
|
|
396
|
+
schema.queryParams.push(paramDef(q.key, { example: q.value, description: q.description }));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const h of req.header ?? []) {
|
|
400
|
+
if (h?.key && !h.disabled)
|
|
401
|
+
schema.headers.push(paramDef(h.key, { example: h.value, description: h.description }));
|
|
402
|
+
}
|
|
403
|
+
return schema;
|
|
404
|
+
}
|
|
325
405
|
var SUPPORTED_METHODS2 = [
|
|
326
406
|
"GET",
|
|
327
407
|
"POST",
|
|
@@ -353,6 +433,7 @@ function parsePostmanToEndpoints(source) {
|
|
|
353
433
|
warnings.push(`Skipping request with no extractable path: ${item.name ?? "(unnamed)"}`);
|
|
354
434
|
return;
|
|
355
435
|
}
|
|
436
|
+
const requestSchema = postmanRequestSchema(item.request);
|
|
356
437
|
const example = item.response?.[0];
|
|
357
438
|
if (example) {
|
|
358
439
|
endpoints.push(
|
|
@@ -362,6 +443,7 @@ function parsePostmanToEndpoints(source) {
|
|
|
362
443
|
method,
|
|
363
444
|
pathPattern: path,
|
|
364
445
|
example: example.name,
|
|
446
|
+
requestSchema,
|
|
365
447
|
response: {
|
|
366
448
|
// Postman's `code` is the canonical numeric status; `status` is a
|
|
367
449
|
// human-readable label that *sometimes* parses as a number. Try
|
|
@@ -379,6 +461,7 @@ function parsePostmanToEndpoints(source) {
|
|
|
379
461
|
name: item.name,
|
|
380
462
|
method,
|
|
381
463
|
pathPattern: path,
|
|
464
|
+
requestSchema,
|
|
382
465
|
response: {
|
|
383
466
|
status: 200,
|
|
384
467
|
headers: [{ key: "Content-Type", value: "application/json" }],
|
|
@@ -422,6 +505,26 @@ function slug2(s) {
|
|
|
422
505
|
}
|
|
423
506
|
|
|
424
507
|
// src/parsers/insomnia.ts
|
|
508
|
+
var import_shared4 = require("@apicircle/shared");
|
|
509
|
+
function extractPathSlots(path) {
|
|
510
|
+
const out = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const m of path.matchAll(/[:{]([A-Za-z0-9_]+)\}?/g)) out.add(m[1]);
|
|
512
|
+
return [...out];
|
|
513
|
+
}
|
|
514
|
+
function insomniaRequestSchema(r, path) {
|
|
515
|
+
const schema = (0, import_shared4.makeDefaultRequestSchema)();
|
|
516
|
+
for (const slot of extractPathSlots(path))
|
|
517
|
+
schema.pathParams.push(paramDef(slot, { required: true }));
|
|
518
|
+
for (const p of r.parameters ?? []) {
|
|
519
|
+
if (p?.name && !p.disabled)
|
|
520
|
+
schema.queryParams.push(paramDef(p.name, { example: p.value, description: p.description }));
|
|
521
|
+
}
|
|
522
|
+
for (const h of r.headers ?? []) {
|
|
523
|
+
if (h?.name && !h.disabled)
|
|
524
|
+
schema.headers.push(paramDef(h.name, { example: h.value, description: h.description }));
|
|
525
|
+
}
|
|
526
|
+
return schema;
|
|
527
|
+
}
|
|
425
528
|
var SUPPORTED_METHODS3 = [
|
|
426
529
|
"GET",
|
|
427
530
|
"POST",
|
|
@@ -459,6 +562,7 @@ function parseInsomniaToEndpoints(source) {
|
|
|
459
562
|
name: r.name,
|
|
460
563
|
method,
|
|
461
564
|
pathPattern: path,
|
|
565
|
+
requestSchema: insomniaRequestSchema(r, path),
|
|
462
566
|
response: {
|
|
463
567
|
status: 200,
|
|
464
568
|
headers: [{ key: "Content-Type", value: "application/json" }],
|
|
@@ -658,12 +762,8 @@ function resolveJsonPath(body, jsonPath) {
|
|
|
658
762
|
if (path.startsWith(".")) path = path.slice(1);
|
|
659
763
|
if (path === "") return body;
|
|
660
764
|
let cursor = body;
|
|
661
|
-
const
|
|
662
|
-
let match;
|
|
663
|
-
while ((match = re.exec(path)) !== null) {
|
|
765
|
+
for (const key of iterJsonPathTokens(path)) {
|
|
664
766
|
if (cursor === void 0 || cursor === null) return void 0;
|
|
665
|
-
const key = match[1] ?? match[2];
|
|
666
|
-
if (key === void 0) return void 0;
|
|
667
767
|
if (Array.isArray(cursor)) {
|
|
668
768
|
const idx = Number(key);
|
|
669
769
|
if (!Number.isInteger(idx)) return void 0;
|
|
@@ -678,6 +778,32 @@ function resolveJsonPath(body, jsonPath) {
|
|
|
678
778
|
}
|
|
679
779
|
return cursor;
|
|
680
780
|
}
|
|
781
|
+
function* iterJsonPathTokens(path) {
|
|
782
|
+
let i = 0;
|
|
783
|
+
while (i < path.length) {
|
|
784
|
+
const ch = path[i];
|
|
785
|
+
if (ch === "." || ch === "]") {
|
|
786
|
+
i++;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (ch === "[") {
|
|
790
|
+
const close = path.indexOf("]", i + 1);
|
|
791
|
+
if (close === -1) return;
|
|
792
|
+
const inner = path.slice(i + 1, close);
|
|
793
|
+
if (inner.length > 0) yield inner;
|
|
794
|
+
i = close + 1;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
let j = i + 1;
|
|
798
|
+
while (j < path.length) {
|
|
799
|
+
const c = path[j];
|
|
800
|
+
if (c === "." || c === "[" || c === "]") break;
|
|
801
|
+
j++;
|
|
802
|
+
}
|
|
803
|
+
yield path.slice(i, j);
|
|
804
|
+
i = j;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
681
807
|
|
|
682
808
|
// src/response/applyMultipliers.ts
|
|
683
809
|
function applyMultipliers(response, ctx) {
|
|
@@ -754,21 +880,39 @@ function parsePathSegments(jsonPath) {
|
|
|
754
880
|
if (path.startsWith(".")) path = path.slice(1);
|
|
755
881
|
if (path === "") return [];
|
|
756
882
|
const out = [];
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
883
|
+
let i = 0;
|
|
884
|
+
while (i < path.length) {
|
|
885
|
+
const ch = path[i];
|
|
886
|
+
if (ch === "." || ch === "]") {
|
|
887
|
+
i++;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (ch === "[") {
|
|
891
|
+
const close = path.indexOf("]", i + 1);
|
|
892
|
+
if (close === -1) break;
|
|
893
|
+
const inner = path.slice(i + 1, close);
|
|
894
|
+
if (inner.length > 0) {
|
|
895
|
+
const n = Number(inner);
|
|
896
|
+
if (Number.isInteger(n)) {
|
|
897
|
+
out.push({ kind: "index", idx: n });
|
|
898
|
+
} else {
|
|
899
|
+
if (FORBIDDEN_KEYS.has(inner)) return [];
|
|
900
|
+
out.push({ kind: "key", name: inner });
|
|
901
|
+
}
|
|
770
902
|
}
|
|
903
|
+
i = close + 1;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
let j = i + 1;
|
|
907
|
+
while (j < path.length) {
|
|
908
|
+
const c = path[j];
|
|
909
|
+
if (c === "." || c === "[" || c === "]") break;
|
|
910
|
+
j++;
|
|
771
911
|
}
|
|
912
|
+
const name = path.slice(i, j);
|
|
913
|
+
if (FORBIDDEN_KEYS.has(name)) return [];
|
|
914
|
+
out.push({ kind: "key", name });
|
|
915
|
+
i = j;
|
|
772
916
|
}
|
|
773
917
|
return out;
|
|
774
918
|
}
|
|
@@ -965,7 +1109,23 @@ function defaultContentTypeFor(bodyType) {
|
|
|
965
1109
|
}
|
|
966
1110
|
}
|
|
967
1111
|
function openApiPathToHono(path) {
|
|
968
|
-
|
|
1112
|
+
let out = "";
|
|
1113
|
+
let i = 0;
|
|
1114
|
+
while (i < path.length) {
|
|
1115
|
+
if (path[i] === "{") {
|
|
1116
|
+
const close = path.indexOf("}", i + 1);
|
|
1117
|
+
if (close === -1) {
|
|
1118
|
+
out += path.slice(i);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
out += ":" + path.slice(i + 1, close);
|
|
1122
|
+
i = close + 1;
|
|
1123
|
+
} else {
|
|
1124
|
+
out += path[i];
|
|
1125
|
+
i++;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
969
1129
|
}
|
|
970
1130
|
|
|
971
1131
|
// src/runtime/nodeAdapter.ts
|
|
@@ -1003,11 +1163,60 @@ async function isPortFree(port) {
|
|
|
1003
1163
|
|
|
1004
1164
|
// src/runtime/nodeAdapter.ts
|
|
1005
1165
|
var CLOSE_TIMEOUT_MS = 3e3;
|
|
1166
|
+
var MockServerStartError = class extends Error {
|
|
1167
|
+
code;
|
|
1168
|
+
port;
|
|
1169
|
+
host;
|
|
1170
|
+
constructor(opts) {
|
|
1171
|
+
super(opts.message);
|
|
1172
|
+
this.name = "MockServerStartError";
|
|
1173
|
+
this.code = opts.code;
|
|
1174
|
+
this.port = opts.port;
|
|
1175
|
+
this.host = opts.host;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
function explainBindError(code, port, host) {
|
|
1179
|
+
switch (code) {
|
|
1180
|
+
case "EADDRINUSE":
|
|
1181
|
+
return `Port ${port} on ${host} is already in use. Stop the other process or pick a different port.`;
|
|
1182
|
+
case "EACCES":
|
|
1183
|
+
return `Permission denied binding to port ${port} on ${host}. Ports below 1024 usually require elevated privileges \u2014 pick a port in 1024\u201365535.`;
|
|
1184
|
+
case "EADDRNOTAVAIL":
|
|
1185
|
+
return `Cannot bind to ${host}:${port} \u2014 that address is not available on this machine.`;
|
|
1186
|
+
default:
|
|
1187
|
+
return `Failed to bind ${host}:${port} (${code}).`;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1006
1190
|
async function serveOnNode(app, opts = {}) {
|
|
1007
|
-
const port = opts.port && opts.port > 0 ? opts.port : await getFreePort();
|
|
1008
1191
|
const host = opts.host ?? "127.0.0.1";
|
|
1192
|
+
let port;
|
|
1193
|
+
if (opts.port === void 0 || opts.port === 0) {
|
|
1194
|
+
port = await getFreePort();
|
|
1195
|
+
} else {
|
|
1196
|
+
if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
|
|
1197
|
+
throw new MockServerStartError({
|
|
1198
|
+
code: "INVALID_PORT",
|
|
1199
|
+
port: opts.port,
|
|
1200
|
+
host,
|
|
1201
|
+
message: `Invalid port ${String(opts.port)} \u2014 must be an integer between 1 and 65535.`
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
port = opts.port;
|
|
1205
|
+
}
|
|
1009
1206
|
let server = null;
|
|
1010
1207
|
await new Promise((resolve, reject) => {
|
|
1208
|
+
const wrapError = (err) => {
|
|
1209
|
+
const candidate = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
1210
|
+
const code = typeof candidate === "string" ? candidate : "UNKNOWN";
|
|
1211
|
+
reject(
|
|
1212
|
+
new MockServerStartError({
|
|
1213
|
+
code,
|
|
1214
|
+
port,
|
|
1215
|
+
host,
|
|
1216
|
+
message: explainBindError(code, port, host)
|
|
1217
|
+
})
|
|
1218
|
+
);
|
|
1219
|
+
};
|
|
1011
1220
|
try {
|
|
1012
1221
|
server = (0, import_node_server.serve)(
|
|
1013
1222
|
{
|
|
@@ -1017,9 +1226,9 @@ async function serveOnNode(app, opts = {}) {
|
|
|
1017
1226
|
},
|
|
1018
1227
|
() => resolve()
|
|
1019
1228
|
);
|
|
1020
|
-
server.on("error",
|
|
1229
|
+
server.on("error", wrapError);
|
|
1021
1230
|
} catch (err) {
|
|
1022
|
-
|
|
1231
|
+
wrapError(err);
|
|
1023
1232
|
}
|
|
1024
1233
|
});
|
|
1025
1234
|
return {
|
|
@@ -1075,6 +1284,7 @@ async function stopMockServer(handle) {
|
|
|
1075
1284
|
}
|
|
1076
1285
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1077
1286
|
0 && (module.exports = {
|
|
1287
|
+
MockServerStartError,
|
|
1078
1288
|
buildRouter,
|
|
1079
1289
|
createMockApp,
|
|
1080
1290
|
getFreePort,
|