api-json-server 1.1.0 → 1.2.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/behavior.js CHANGED
@@ -1,8 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDelay = resolveDelay;
3
4
  exports.sleep = sleep;
4
5
  exports.shouldFail = shouldFail;
5
6
  exports.resolveBehavior = resolveBehavior;
7
+ const faker_1 = require("@faker-js/faker");
8
+ /**
9
+ * Resolve a delay value from either a number or a range configuration.
10
+ */
11
+ function resolveDelay(delay) {
12
+ if (!delay)
13
+ return 0;
14
+ if (typeof delay === "number")
15
+ return delay;
16
+ return faker_1.faker.number.int({ min: delay.min, max: delay.max });
17
+ }
6
18
  /**
7
19
  * Pause for the given number of milliseconds.
8
20
  */
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HistoryRecorder = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * In-memory request history recorder.
7
+ */
8
+ class HistoryRecorder {
9
+ entries = [];
10
+ maxEntries;
11
+ /**
12
+ * Create a new history recorder.
13
+ * @param maxEntries Maximum number of entries to keep (default 1000).
14
+ */
15
+ constructor(maxEntries = 1000) {
16
+ this.maxEntries = maxEntries;
17
+ }
18
+ /**
19
+ * Record a new request.
20
+ */
21
+ record(entry) {
22
+ const fullEntry = {
23
+ id: (0, node_crypto_1.randomUUID)(),
24
+ timestamp: new Date().toISOString(),
25
+ ...entry
26
+ };
27
+ this.entries.push(fullEntry);
28
+ // Keep only the most recent entries
29
+ if (this.entries.length > this.maxEntries) {
30
+ this.entries.shift();
31
+ }
32
+ return fullEntry;
33
+ }
34
+ /**
35
+ * Get all history entries, optionally filtered.
36
+ */
37
+ query(filter) {
38
+ let results = this.entries;
39
+ if (filter?.endpoint) {
40
+ results = results.filter((e) => e.path === filter.endpoint);
41
+ }
42
+ if (filter?.method) {
43
+ results = results.filter((e) => e.method.toUpperCase() === filter.method?.toUpperCase());
44
+ }
45
+ if (filter?.statusCode !== undefined) {
46
+ results = results.filter((e) => e.statusCode === filter.statusCode);
47
+ }
48
+ if (filter?.limit && filter.limit > 0) {
49
+ results = results.slice(-filter.limit);
50
+ }
51
+ return results;
52
+ }
53
+ /**
54
+ * Clear all history entries.
55
+ */
56
+ clear() {
57
+ this.entries = [];
58
+ }
59
+ /**
60
+ * Get the total number of recorded entries.
61
+ */
62
+ count() {
63
+ return this.entries.length;
64
+ }
65
+ }
66
+ exports.HistoryRecorder = HistoryRecorder;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
5
  const node_fs_1 = require("node:fs");
6
+ const customLogger_js_1 = require("./logger/customLogger.js");
6
7
  const program = new commander_1.Command();
7
8
  program
8
9
  .name("mockserve")
@@ -16,6 +17,8 @@ program
16
17
  .option("--watch", "Reload when spec file changes", true)
17
18
  .option("--no-watch", "Disable reload when spec file changes")
18
19
  .option("--base-url <url>", "Public base URL used in OpenAPI servers[] (e.g. https://example.com)")
20
+ .option("--log-format <format>", "Log format: pretty or json", "pretty")
21
+ .option("--log-level <level>", "Log level: trace, debug, info, warn, error, fatal", "info")
19
22
  .action(async (opts) => {
20
23
  await startCommand(opts);
21
24
  });
@@ -29,6 +32,16 @@ async function startCommand(opts) {
29
32
  process.exit(1);
30
33
  }
31
34
  const specPath = opts.spec;
35
+ // Create logger based on CLI options
36
+ const logFormat = opts.logFormat === "json" ? "json" : "pretty";
37
+ const logLevel = ["trace", "debug", "info", "warn", "error", "fatal"].includes(opts.logLevel)
38
+ ? opts.logLevel
39
+ : "info";
40
+ const logger = (0, customLogger_js_1.createLogger)({
41
+ enabled: true,
42
+ format: logFormat,
43
+ level: logLevel
44
+ });
32
45
  const { loadSpecFromFile } = await import("./loadSpec.js");
33
46
  const { buildServer } = await import("./server.js");
34
47
  let app = null;
@@ -40,8 +53,8 @@ async function startCommand(opts) {
40
53
  async function startWithSpec() {
41
54
  const loadedAt = new Date().toISOString();
42
55
  const spec = await loadSpecFromFile(specPath);
43
- console.log(`Loaded spec v${spec.version} with ${spec.endpoints.length} endpoint(s).`);
44
- const nextApp = buildServer(spec, { specPath, loadedAt, baseUrl: opts.baseUrl });
56
+ logger.info(`Loaded spec v${spec.version} with ${spec.endpoints.length} endpoint(s).`);
57
+ const nextApp = buildServer(spec, { specPath, loadedAt, baseUrl: opts.baseUrl, logger: true });
45
58
  try {
46
59
  await nextApp.listen({ port, host: "0.0.0.0" });
47
60
  }
@@ -49,8 +62,7 @@ async function startCommand(opts) {
49
62
  nextApp.log.error(err);
50
63
  throw err;
51
64
  }
52
- nextApp.log.info(`Mock server running on http://localhost:${port}`);
53
- nextApp.log.info(`Spec: ${specPath} (loadedAt=${loadedAt})`);
65
+ (0, customLogger_js_1.logServerStart)(nextApp.log, port, specPath);
54
66
  return nextApp;
55
67
  }
56
68
  /**
@@ -61,33 +73,32 @@ async function startCommand(opts) {
61
73
  return;
62
74
  isReloading = true;
63
75
  try {
64
- console.log("Reloading spec...");
76
+ logger.info("Reloading spec...");
65
77
  // 1) Stop accepting requests on the old server FIRST
66
78
  if (app) {
67
- console.log("Closing current server...");
79
+ logger.debug("Closing current server...");
68
80
  await app.close();
69
- console.log("Current server closed.");
81
+ logger.debug("Current server closed.");
70
82
  app = null;
71
83
  }
72
84
  // 2) Start a new server on the same port with the updated spec
73
85
  app = await startWithSpec();
74
- console.log("Reload complete.");
86
+ (0, customLogger_js_1.logServerReload)(logger, true);
75
87
  }
76
88
  catch (err) {
77
- console.error("Reload failed.");
78
- // At this point the old server may already be closed. We want visibility.
79
- console.error(String(err));
89
+ const errorMsg = err instanceof Error ? err.message : String(err);
90
+ (0, customLogger_js_1.logServerReload)(logger, false, errorMsg);
80
91
  // Optional: try to start again to avoid being down
81
92
  try {
82
93
  if (!app) {
83
- console.log("Attempting to start server again after reload failure...");
94
+ logger.info("Attempting to start server again after reload failure...");
84
95
  app = await startWithSpec();
85
- console.log("Recovery start succeeded.");
96
+ logger.info("Recovery start succeeded.");
86
97
  }
87
98
  }
88
99
  catch (err2) {
89
- console.error("Recovery start failed. Server is down until next successful reload.");
90
- console.error(String(err2));
100
+ logger.error("Recovery start failed. Server is down until next successful reload.");
101
+ logger.error(err2);
91
102
  }
92
103
  }
93
104
  finally {
@@ -99,7 +110,7 @@ async function startCommand(opts) {
99
110
  app = await startWithSpec();
100
111
  }
101
112
  catch (err) {
102
- console.error(String(err));
113
+ logger.error(err);
103
114
  process.exit(1);
104
115
  }
105
116
  // Watch spec for changes
@@ -110,12 +121,12 @@ async function startCommand(opts) {
110
121
  debounceTimer = scheduleReload(reload, debounceTimer);
111
122
  }
112
123
  if (opts.watch) {
113
- console.log(`Watching spec file for changes: ${specPath}`);
124
+ logger.info(`Watching spec file for changes: ${specPath}`);
114
125
  // fs.watch emits multiple events; debounce to avoid rapid reload loops
115
126
  (0, node_fs_1.watch)(specPath, onSpecChange);
116
127
  }
117
128
  else {
118
- console.log("Watch disabled (--no-watch).");
129
+ logger.info("Watch disabled (--no-watch).");
119
130
  }
120
131
  }
121
132
  /**
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createLogger = createLogger;
7
+ exports.logRequest = logRequest;
8
+ exports.logServerStart = logServerStart;
9
+ exports.logServerReload = logServerReload;
10
+ exports.logEndpointRegistered = logEndpointRegistered;
11
+ const pino_1 = __importDefault(require("pino"));
12
+ const formatters_js_1 = require("./formatters.js");
13
+ /**
14
+ * Create a custom logger for the mock server.
15
+ */
16
+ function createLogger(options) {
17
+ if (!options.enabled) {
18
+ return (0, pino_1.default)({ level: "silent" });
19
+ }
20
+ if (options.format === "json") {
21
+ return (0, pino_1.default)({
22
+ level: options.level,
23
+ timestamp: pino_1.default.stdTimeFunctions.isoTime
24
+ });
25
+ }
26
+ // Pretty format with custom output (simplified to avoid serialization issues in tests)
27
+ return (0, pino_1.default)({
28
+ level: options.level,
29
+ transport: {
30
+ target: "pino-pretty",
31
+ options: {
32
+ colorize: true,
33
+ translateTime: "HH:MM:ss",
34
+ ignore: "pid,hostname",
35
+ messageFormat: "{msg}"
36
+ }
37
+ }
38
+ });
39
+ }
40
+ /**
41
+ * Format and log a request/response pair.
42
+ */
43
+ function logRequest(logger, method, url, statusCode, responseTime) {
44
+ const formattedMethod = (0, formatters_js_1.formatMethod)(method);
45
+ const formattedStatus = (0, formatters_js_1.formatStatusCode)(statusCode);
46
+ const formattedTime = (0, formatters_js_1.formatResponseTime)(responseTime);
47
+ logger.info(`${formattedMethod} ${url} ${formattedStatus} ${formattedTime}`);
48
+ }
49
+ /**
50
+ * Log server startup.
51
+ */
52
+ function logServerStart(logger, port, specPath) {
53
+ logger.info(`🚀 Mock server running on http://localhost:${port}`);
54
+ logger.info(`📄 Spec: ${specPath}`);
55
+ logger.info(`📖 Docs: http://localhost:${port}/docs`);
56
+ }
57
+ /**
58
+ * Log server reload.
59
+ */
60
+ function logServerReload(logger, success, error) {
61
+ if (success) {
62
+ logger.info("✅ Spec reloaded successfully");
63
+ }
64
+ else {
65
+ logger.error(`❌ Reload failed: ${error}`);
66
+ }
67
+ }
68
+ /**
69
+ * Log endpoint registration.
70
+ */
71
+ function logEndpointRegistered(logger, method, path, status) {
72
+ const formattedMethod = (0, formatters_js_1.formatMethod)(method);
73
+ const statusInfo = status ? ` → ${status}` : "";
74
+ logger.debug(`Registered ${formattedMethod} ${path}${statusInfo}`);
75
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.formatStatusCode = formatStatusCode;
7
+ exports.formatMethod = formatMethod;
8
+ exports.formatResponseTime = formatResponseTime;
9
+ exports.formatTimestamp = formatTimestamp;
10
+ exports.formatLogLevel = formatLogLevel;
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ /**
13
+ * Format HTTP status code with color based on status range.
14
+ */
15
+ function formatStatusCode(statusCode) {
16
+ if (statusCode >= 500) {
17
+ return chalk_1.default.red(statusCode.toString());
18
+ }
19
+ if (statusCode >= 400) {
20
+ return chalk_1.default.yellow(statusCode.toString());
21
+ }
22
+ if (statusCode >= 300) {
23
+ return chalk_1.default.cyan(statusCode.toString());
24
+ }
25
+ if (statusCode >= 200) {
26
+ return chalk_1.default.green(statusCode.toString());
27
+ }
28
+ return chalk_1.default.white(statusCode.toString());
29
+ }
30
+ /**
31
+ * Format HTTP method with color.
32
+ */
33
+ function formatMethod(method) {
34
+ const colors = {
35
+ GET: chalk_1.default.green,
36
+ POST: chalk_1.default.blue,
37
+ PUT: chalk_1.default.yellow,
38
+ PATCH: chalk_1.default.magenta,
39
+ DELETE: chalk_1.default.red,
40
+ HEAD: chalk_1.default.gray,
41
+ OPTIONS: chalk_1.default.cyan
42
+ };
43
+ const formatter = colors[method.toUpperCase()] || chalk_1.default.white;
44
+ return formatter(method.toUpperCase().padEnd(7));
45
+ }
46
+ /**
47
+ * Format response time with color based on duration.
48
+ */
49
+ function formatResponseTime(ms) {
50
+ const formatted = `${ms.toFixed(2)}ms`;
51
+ if (ms > 1000)
52
+ return chalk_1.default.red(formatted);
53
+ if (ms > 500)
54
+ return chalk_1.default.yellow(formatted);
55
+ if (ms > 100)
56
+ return chalk_1.default.cyan(formatted);
57
+ return chalk_1.default.green(formatted);
58
+ }
59
+ /**
60
+ * Format timestamp in readable format.
61
+ */
62
+ function formatTimestamp(date) {
63
+ const hours = date.getHours().toString().padStart(2, "0");
64
+ const minutes = date.getMinutes().toString().padStart(2, "0");
65
+ const seconds = date.getSeconds().toString().padStart(2, "0");
66
+ return chalk_1.default.gray(`[${hours}:${minutes}:${seconds}]`);
67
+ }
68
+ /**
69
+ * Format a log level with appropriate color.
70
+ */
71
+ function formatLogLevel(level) {
72
+ const colors = {
73
+ trace: chalk_1.default.gray,
74
+ debug: chalk_1.default.cyan,
75
+ info: chalk_1.default.blue,
76
+ warn: chalk_1.default.yellow,
77
+ error: chalk_1.default.red,
78
+ fatal: chalk_1.default.bgRed.white
79
+ };
80
+ const formatter = colors[level.toLowerCase()] || chalk_1.default.white;
81
+ return formatter(level.toUpperCase().padEnd(5));
82
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -4,6 +4,7 @@ exports.registerEndpoints = registerEndpoints;
4
4
  const requestMatch_js_1 = require("./requestMatch.js");
5
5
  const responseRenderer_js_1 = require("./responseRenderer.js");
6
6
  const behavior_js_1 = require("./behavior.js");
7
+ const customLogger_js_1 = require("./logger/customLogger.js");
7
8
  /**
8
9
  * Select a response source from the first matching variant.
9
10
  */
@@ -15,6 +16,8 @@ function selectVariant(req, endpoint) {
15
16
  return {
16
17
  status: variant.status,
17
18
  response: variant.response,
19
+ headers: variant.headers,
20
+ delay: variant.delay,
18
21
  delayMs: variant.delayMs,
19
22
  errorRate: variant.errorRate,
20
23
  errorStatus: variant.errorStatus,
@@ -31,6 +34,8 @@ function selectEndpointSource(endpoint) {
31
34
  return {
32
35
  status: endpoint.status,
33
36
  response: endpoint.response,
37
+ headers: endpoint.headers,
38
+ delay: endpoint.delay,
34
39
  delayMs: endpoint.delayMs,
35
40
  errorRate: endpoint.errorRate,
36
41
  errorStatus: endpoint.errorStatus,
@@ -47,7 +52,7 @@ function registerEndpoints(app, spec) {
47
52
  url: endpoint.path,
48
53
  handler: buildEndpointHandler(spec, endpoint)
49
54
  });
50
- app.log.info(`Registered ${endpoint.method} ${endpoint.path} -> ${endpoint.status} (delay=${endpoint.delayMs ?? spec.settings.delayMs}ms, errorRate=${endpoint.errorRate ?? spec.settings.errorRate})`);
55
+ (0, customLogger_js_1.logEndpointRegistered)(app.log, endpoint.method, endpoint.path, endpoint.status);
51
56
  }
52
57
  }
53
58
  /**
@@ -62,8 +67,10 @@ function buildEndpointHandler(spec, endpoint) {
62
67
  }
63
68
  const source = variant ?? selectEndpointSource(endpoint);
64
69
  const behavior = (0, behavior_js_1.resolveBehavior)(spec.settings, endpoint, source);
65
- if (behavior.delayMs > 0) {
66
- await (0, behavior_js_1.sleep)(behavior.delayMs);
70
+ // Resolve delay (supports both delay and delayMs, with delay taking precedence)
71
+ const delayValue = source.delay ? (0, behavior_js_1.resolveDelay)(source.delay) : behavior.delayMs;
72
+ if (delayValue > 0) {
73
+ await (0, behavior_js_1.sleep)(delayValue);
67
74
  }
68
75
  const params = (0, requestMatch_js_1.toRecord)(req.params);
69
76
  const query = (0, requestMatch_js_1.toRecord)(req.query);
@@ -73,6 +80,16 @@ function buildEndpointHandler(spec, endpoint) {
73
80
  reply.code(behavior.errorStatus);
74
81
  return (0, responseRenderer_js_1.renderTemplateValue)(behavior.errorResponse, renderContext);
75
82
  }
83
+ // Apply custom headers (with template support)
84
+ const headers = source.headers ?? endpoint.headers;
85
+ if (headers) {
86
+ for (const [key, value] of Object.entries(headers)) {
87
+ const renderedValue = typeof value === "string"
88
+ ? String((0, responseRenderer_js_1.renderTemplateValue)(value, renderContext))
89
+ : String(value);
90
+ reply.header(key, renderedValue);
91
+ }
92
+ }
76
93
  const rendered = (0, responseRenderer_js_1.renderTemplateValue)(source.response, renderContext);
77
94
  reply.code(source.status ?? endpoint.status ?? 200);
78
95
  return rendered;
@@ -47,7 +47,42 @@ function bodyMatches(req, expected) {
47
47
  return true;
48
48
  }
49
49
  /**
50
- * Check if a request matches query/body rules.
50
+ * Check if the request headers match the expected values (case-insensitive).
51
+ */
52
+ function headersMatch(req, expected) {
53
+ if (!expected)
54
+ return true;
55
+ // Normalize header keys to lowercase for case-insensitive matching
56
+ const headers = new Map();
57
+ for (const [key, value] of Object.entries(req.headers)) {
58
+ if (typeof value === "string") {
59
+ headers.set(key.toLowerCase(), value);
60
+ }
61
+ }
62
+ for (const [key, exp] of Object.entries(expected)) {
63
+ const actual = headers.get(key.toLowerCase());
64
+ if (actual !== exp)
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ /**
70
+ * Check if the request cookies match the expected values.
71
+ */
72
+ function cookiesMatch(req, expected) {
73
+ if (!expected)
74
+ return true;
75
+ const cookies = req.cookies;
76
+ if (!cookies)
77
+ return false;
78
+ for (const [key, exp] of Object.entries(expected)) {
79
+ if (cookies[key] !== exp)
80
+ return false;
81
+ }
82
+ return true;
83
+ }
84
+ /**
85
+ * Check if a request matches query/body/headers/cookies rules.
51
86
  */
52
87
  function matchRequest(req, match) {
53
88
  if (!match)
@@ -56,5 +91,9 @@ function matchRequest(req, match) {
56
91
  return false;
57
92
  if (!bodyMatches(req, match.body))
58
93
  return false;
94
+ if (!headersMatch(req, match.headers))
95
+ return false;
96
+ if (!cookiesMatch(req, match.cookies))
97
+ return false;
59
98
  return true;
60
99
  }
package/dist/server.js CHANGED
@@ -9,7 +9,10 @@ const registerEndpoints_js_1 = require("./registerEndpoints.js");
9
9
  const swagger_ui_dist_1 = __importDefault(require("swagger-ui-dist"));
10
10
  const openapi_js_1 = require("./openapi.js");
11
11
  const static_1 = __importDefault(require("@fastify/static"));
12
+ const cookie_1 = __importDefault(require("@fastify/cookie"));
13
+ const cors_1 = __importDefault(require("@fastify/cors"));
12
14
  const yaml_1 = __importDefault(require("yaml"));
15
+ const historyRecorder_js_1 = require("./history/historyRecorder.js");
13
16
  /**
14
17
  * Resolve the path to swagger-ui-dist assets.
15
18
  */
@@ -40,8 +43,50 @@ function resolveServerUrl(req, baseUrl) {
40
43
  */
41
44
  function buildServer(spec, meta) {
42
45
  const app = (0, fastify_1.default)({
43
- logger: true,
44
- trustProxy: true
46
+ logger: meta?.logger ?? true,
47
+ trustProxy: true,
48
+ disableRequestLogging: false
49
+ });
50
+ // Register CORS if configured
51
+ if (spec.settings.cors) {
52
+ app.register(cors_1.default, {
53
+ origin: spec.settings.cors.origin ?? true,
54
+ credentials: spec.settings.cors.credentials ?? false,
55
+ methods: spec.settings.cors.methods,
56
+ allowedHeaders: spec.settings.cors.allowedHeaders,
57
+ exposedHeaders: spec.settings.cors.exposedHeaders,
58
+ maxAge: spec.settings.cors.maxAge
59
+ });
60
+ }
61
+ // Register cookie parser plugin
62
+ app.register(cookie_1.default);
63
+ // Create history recorder
64
+ const history = new historyRecorder_js_1.HistoryRecorder(1000);
65
+ // Record all requests in onRequest hook (after body parsing)
66
+ app.addHook("preHandler", async (req, reply) => {
67
+ const startTime = Date.now();
68
+ // Store start time for response hook
69
+ req.startTime = startTime;
70
+ history.record({
71
+ method: req.method,
72
+ url: req.url,
73
+ path: req.routeOptions?.url ?? req.url.split("?")[0],
74
+ query: req.query,
75
+ headers: req.headers,
76
+ body: req.body
77
+ });
78
+ });
79
+ // Update history with response details in onResponse hook
80
+ app.addHook("onResponse", async (req, reply) => {
81
+ const startTime = req.startTime ?? Date.now();
82
+ const responseTime = Date.now() - startTime;
83
+ // Find and update the last entry (just added in onRequest)
84
+ const entries = history.query({ limit: 1 });
85
+ if (entries.length > 0) {
86
+ const lastEntry = entries[entries.length - 1];
87
+ lastEntry.statusCode = reply.statusCode;
88
+ lastEntry.responseTime = responseTime;
89
+ }
45
90
  });
46
91
  /**
47
92
  * Handler for the /__spec route with bound metadata.
@@ -79,6 +124,21 @@ function buildServer(spec, meta) {
79
124
  decorateReply: false
80
125
  });
81
126
  app.get("/docs", docsRouteHandler);
127
+ // History endpoints
128
+ app.get("/__history", async (req) => {
129
+ const query = req.query;
130
+ const filter = {
131
+ endpoint: query.endpoint,
132
+ method: query.method,
133
+ statusCode: query.statusCode ? Number(query.statusCode) : undefined,
134
+ limit: query.limit ? Number(query.limit) : undefined
135
+ };
136
+ return { entries: history.query(filter), total: history.count() };
137
+ });
138
+ app.delete("/__history", async () => {
139
+ history.clear();
140
+ return { ok: true, message: "History cleared" };
141
+ });
82
142
  (0, registerEndpoints_js_1.registerEndpoints)(app, spec);
83
143
  return app;
84
144
  }
package/dist/spec.js CHANGED
@@ -68,15 +68,28 @@ const TemplateValueSchema = z.lazy(() => z.union([
68
68
  exports.MatchSchema = z.object({
69
69
  query: z.record(z.string(), PrimitiveSchema).optional(),
70
70
  // Exact match for top-level body fields only (keeps v1 simple)
71
- body: z.record(z.string(), PrimitiveSchema).optional()
71
+ body: z.record(z.string(), PrimitiveSchema).optional(),
72
+ // Header matching (case-insensitive keys)
73
+ headers: z.record(z.string(), z.string()).optional(),
74
+ // Cookie matching
75
+ cookies: z.record(z.string(), z.string()).optional()
72
76
  });
77
+ const DelaySchema = z.union([
78
+ z.number().int().min(0),
79
+ z.object({
80
+ min: z.number().int().min(0),
81
+ max: z.number().int().min(0)
82
+ })
83
+ ]);
73
84
  exports.VariantSchema = z.object({
74
85
  name: z.string().min(1).optional(),
75
86
  match: exports.MatchSchema.optional(),
76
87
  status: z.number().int().min(100).max(599).optional(),
77
88
  response: TemplateValueSchema,
89
+ headers: z.record(z.string(), z.string()).optional(),
78
90
  // Simulation overrides per variant (optional)
79
91
  delayMs: z.number().int().min(0).optional(),
92
+ delay: DelaySchema.optional(),
80
93
  errorRate: z.number().min(0).max(1).optional(),
81
94
  errorStatus: z.number().int().min(100).max(599).optional(),
82
95
  errorResponse: TemplateValueSchema.optional()
@@ -89,8 +102,21 @@ exports.EndpointSchema = z.object({
89
102
  // Response behavior:
90
103
  status: z.number().int().min(200).max(599).default(200),
91
104
  response: TemplateValueSchema,
105
+ headers: z.record(z.string(), z.string()).optional(),
106
+ // Per-endpoint CORS override
107
+ cors: z
108
+ .object({
109
+ origin: z.union([z.string(), z.array(z.string()), z.boolean()]).optional(),
110
+ credentials: z.boolean().optional(),
111
+ methods: z.array(z.string()).optional(),
112
+ allowedHeaders: z.array(z.string()).optional(),
113
+ exposedHeaders: z.array(z.string()).optional(),
114
+ maxAge: z.number().int().optional()
115
+ })
116
+ .optional(),
92
117
  // Simulation (optional overrides)
93
118
  delayMs: z.number().int().min(0).optional(),
119
+ delay: DelaySchema.optional(),
94
120
  errorRate: z.number().min(0).max(1).optional(),
95
121
  errorStatus: z.number().int().min(100).max(599).optional(),
96
122
  errorResponse: TemplateValueSchema.optional()
@@ -103,7 +129,17 @@ exports.MockSpecSchema = z.object({
103
129
  errorRate: z.number().min(0).max(1).default(0),
104
130
  errorStatus: z.number().int().min(100).max(599).default(500),
105
131
  errorResponse: TemplateValueSchema.default({ error: "Mock error" }),
106
- fakerSeed: z.number().int().min(0).optional()
132
+ fakerSeed: z.number().int().min(0).optional(),
133
+ cors: z
134
+ .object({
135
+ origin: z.union([z.string(), z.array(z.string()), z.boolean()]).optional(),
136
+ credentials: z.boolean().optional(),
137
+ methods: z.array(z.string()).optional(),
138
+ allowedHeaders: z.array(z.string()).optional(),
139
+ exposedHeaders: z.array(z.string()).optional(),
140
+ maxAge: z.number().int().optional()
141
+ })
142
+ .optional()
107
143
  })
108
144
  .default({ delayMs: 0, errorRate: 0, errorStatus: 500, errorResponse: { error: "Mock error" } }),
109
145
  endpoints: z.array(exports.EndpointSchema).min(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-json-server",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,9 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@faker-js/faker": "^10.2.0",
33
+ "@fastify/cookie": "^11.0.2",
34
+ "@fastify/cors": "^11.2.0",
35
+ "@fastify/http-proxy": "^11.4.1",
33
36
  "@fastify/static": "^9.0.0",
37
+ "chalk": "^5.6.2",
34
38
  "commander": "^14.0.2",
35
39
  "fastify": "^5.7.1",
40
+ "pino-pretty": "^13.1.3",
36
41
  "swagger-ui-dist": "^5.31.0",
37
42
  "yaml": "^2.8.2",
38
43
  "zod": "^4.3.5"