api-json-server 1.0.1 → 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.
Files changed (51) hide show
  1. package/README.md +636 -0
  2. package/dist/behavior.js +44 -0
  3. package/dist/history/historyRecorder.js +66 -0
  4. package/dist/history/types.js +2 -0
  5. package/dist/index.js +126 -5
  6. package/dist/loadSpec.js +32 -0
  7. package/dist/logger/customLogger.js +75 -0
  8. package/dist/logger/formatters.js +82 -0
  9. package/dist/logger/types.js +2 -0
  10. package/dist/openapi.js +152 -0
  11. package/dist/registerEndpoints.js +97 -0
  12. package/dist/requestMatch.js +99 -0
  13. package/dist/responseRenderer.js +98 -0
  14. package/dist/server.js +210 -0
  15. package/dist/spec.js +146 -0
  16. package/dist/stringTemplate.js +55 -0
  17. package/examples/auth-variants.json +31 -0
  18. package/examples/basic-crud.json +46 -0
  19. package/examples/companies-nested.json +47 -0
  20. package/examples/orders-and-matches.json +49 -0
  21. package/examples/users-faker.json +35 -0
  22. package/mock.spec.json +1 -0
  23. package/mockserve.spec.schema.json +7 -0
  24. package/package.json +20 -3
  25. package/scripts/build-schema.ts +21 -0
  26. package/src/behavior.ts +56 -0
  27. package/src/history/historyRecorder.ts +77 -0
  28. package/src/history/types.ts +25 -0
  29. package/src/index.ts +124 -85
  30. package/src/loadSpec.ts +5 -2
  31. package/src/logger/customLogger.ts +85 -0
  32. package/src/logger/formatters.ts +74 -0
  33. package/src/logger/types.ts +30 -0
  34. package/src/openapi.ts +203 -0
  35. package/src/registerEndpoints.ts +94 -162
  36. package/src/requestMatch.ts +104 -0
  37. package/src/responseRenderer.ts +112 -0
  38. package/src/server.ts +236 -14
  39. package/src/spec.ts +108 -8
  40. package/src/stringTemplate.ts +55 -0
  41. package/tests/behavior.test.ts +88 -0
  42. package/tests/cors.test.ts +128 -0
  43. package/tests/faker.test.ts +175 -0
  44. package/tests/fixtures/spec.basic.json +39 -0
  45. package/tests/headers.test.ts +124 -0
  46. package/tests/helpers.ts +28 -0
  47. package/tests/history.test.ts +188 -0
  48. package/tests/matching.test.ts +245 -0
  49. package/tests/server.test.ts +73 -0
  50. package/tests/template.test.ts +90 -0
  51. package/src/template.ts +0 -61
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toRecord = toRecord;
4
+ exports.matchRequest = matchRequest;
5
+ /**
6
+ * Safely coerce a value to a plain object record.
7
+ */
8
+ /**
9
+ * Safely coerce a value to a plain object record.
10
+ */
11
+ function toRecord(value) {
12
+ if (value && typeof value === "object" && !Array.isArray(value))
13
+ return value;
14
+ return {};
15
+ }
16
+ /**
17
+ * Check if the request query matches the expected values.
18
+ */
19
+ function queryMatches(req, expected) {
20
+ if (!expected)
21
+ return true;
22
+ const query = toRecord(req.query);
23
+ for (const [key, exp] of Object.entries(expected)) {
24
+ const actual = query[key];
25
+ if (Array.isArray(actual))
26
+ return false;
27
+ if (String(actual ?? "") !== String(exp))
28
+ return false;
29
+ }
30
+ return true;
31
+ }
32
+ /**
33
+ * Check if the request body matches the expected top-level values.
34
+ */
35
+ function bodyMatches(req, expected) {
36
+ if (!expected)
37
+ return true;
38
+ const body = req.body;
39
+ if (!body || typeof body !== "object" || Array.isArray(body))
40
+ return false;
41
+ const record = body;
42
+ for (const [key, exp] of Object.entries(expected)) {
43
+ const actual = record[key];
44
+ if (String(actual ?? "") !== String(exp))
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ /**
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.
86
+ */
87
+ function matchRequest(req, match) {
88
+ if (!match)
89
+ return true;
90
+ if (!queryMatches(req, match.query))
91
+ return false;
92
+ if (!bodyMatches(req, match.body))
93
+ return false;
94
+ if (!headersMatch(req, match.headers))
95
+ return false;
96
+ if (!cookiesMatch(req, match.cookies))
97
+ return false;
98
+ return true;
99
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRenderContext = createRenderContext;
4
+ exports.renderTemplateValue = renderTemplateValue;
5
+ const faker_1 = require("@faker-js/faker");
6
+ const stringTemplate_js_1 = require("./stringTemplate.js");
7
+ /**
8
+ * Create a render context with an optional faker seed applied.
9
+ */
10
+ function createRenderContext(ctx, fakerSeed) {
11
+ if (typeof fakerSeed === "number") {
12
+ faker_1.faker.seed(fakerSeed);
13
+ }
14
+ return { ...ctx, faker: faker_1.faker };
15
+ }
16
+ /**
17
+ * Check if a template value is a faker directive.
18
+ */
19
+ function isFakerTemplate(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "__faker" in value;
21
+ }
22
+ /**
23
+ * Check if a template value is a repeat directive.
24
+ */
25
+ function isRepeatTemplate(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "__repeat" in value;
27
+ }
28
+ /**
29
+ * Resolve a faker method path (e.g. "person.firstName") to a callable function.
30
+ */
31
+ function resolveFakerMethod(faker, methodPath) {
32
+ const parts = methodPath.split(".");
33
+ let current = faker;
34
+ for (const part of parts) {
35
+ if (current === null || current === undefined) {
36
+ throw new Error(`Faker method not found: ${methodPath}`);
37
+ }
38
+ if (typeof current !== "object" && typeof current !== "function") {
39
+ throw new Error(`Faker method not found: ${methodPath}`);
40
+ }
41
+ const record = current;
42
+ current = record[part];
43
+ }
44
+ if (typeof current !== "function") {
45
+ throw new Error(`Faker method is not callable: ${methodPath}`);
46
+ }
47
+ return current;
48
+ }
49
+ /**
50
+ * Render a faker directive to a concrete value.
51
+ */
52
+ function renderFakerTemplate(template, ctx) {
53
+ const faker = ctx.faker;
54
+ const raw = template.__faker;
55
+ const methodPath = typeof raw === "string" ? raw : raw.method;
56
+ const args = typeof raw === "string" ? [] : raw.args ?? [];
57
+ const renderedArgs = args.map((arg) => renderTemplateValue(arg, ctx));
58
+ const method = resolveFakerMethod(faker, methodPath);
59
+ return method(...renderedArgs);
60
+ }
61
+ /**
62
+ * Render a repeat directive to an array of rendered items.
63
+ */
64
+ function renderRepeatTemplate(template, ctx) {
65
+ const faker = ctx.faker;
66
+ const { count, min, max, template: itemTemplate } = template.__repeat;
67
+ const minValue = typeof min === "number" ? min : 0;
68
+ if (typeof count === "number") {
69
+ return Array.from({ length: count }, () => renderTemplateValue(itemTemplate, ctx));
70
+ }
71
+ const upper = typeof max === "number" ? max : minValue;
72
+ if (upper < minValue) {
73
+ throw new Error(`Repeat max must be >= min (min=${minValue}, max=${upper})`);
74
+ }
75
+ const total = faker.number.int({ min: minValue, max: upper });
76
+ return Array.from({ length: total }, () => renderTemplateValue(itemTemplate, ctx));
77
+ }
78
+ /**
79
+ * Render a template value into a concrete JSON-compatible value.
80
+ */
81
+ function renderTemplateValue(value, ctx) {
82
+ if (typeof value === "string")
83
+ return (0, stringTemplate_js_1.renderStringTemplate)(value, ctx);
84
+ if (typeof value === "number" || typeof value === "boolean" || value === null)
85
+ return value;
86
+ if (Array.isArray(value)) {
87
+ return value.map((item) => renderTemplateValue(item, ctx));
88
+ }
89
+ if (isFakerTemplate(value))
90
+ return renderFakerTemplate(value, ctx);
91
+ if (isRepeatTemplate(value))
92
+ return renderRepeatTemplate(value, ctx);
93
+ const output = {};
94
+ for (const [key, item] of Object.entries(value)) {
95
+ output[key] = renderTemplateValue(item, ctx);
96
+ }
97
+ return output;
98
+ }
package/dist/server.js ADDED
@@ -0,0 +1,210 @@
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.buildServer = buildServer;
7
+ const fastify_1 = __importDefault(require("fastify"));
8
+ const registerEndpoints_js_1 = require("./registerEndpoints.js");
9
+ const swagger_ui_dist_1 = __importDefault(require("swagger-ui-dist"));
10
+ const openapi_js_1 = require("./openapi.js");
11
+ const static_1 = __importDefault(require("@fastify/static"));
12
+ const cookie_1 = __importDefault(require("@fastify/cookie"));
13
+ const cors_1 = __importDefault(require("@fastify/cors"));
14
+ const yaml_1 = __importDefault(require("yaml"));
15
+ const historyRecorder_js_1 = require("./history/historyRecorder.js");
16
+ /**
17
+ * Resolve the path to swagger-ui-dist assets.
18
+ */
19
+ function getSwaggerUiRoot() {
20
+ // swagger-ui-dist can be CJS or ESM depending on environment.
21
+ // CJS: require("swagger-ui-dist").getAbsoluteFSPath()
22
+ // ESM: default export may itself be the function.
23
+ const mod = swagger_ui_dist_1.default;
24
+ if (typeof mod === "function")
25
+ return mod();
26
+ if (mod && typeof mod.getAbsoluteFSPath === "function") {
27
+ return mod.getAbsoluteFSPath();
28
+ }
29
+ throw new Error("swagger-ui-dist: cannot determine absolute FS path to dist assets");
30
+ }
31
+ /**
32
+ * Resolve the server URL for OpenAPI output.
33
+ */
34
+ function resolveServerUrl(req, baseUrl) {
35
+ if (baseUrl && baseUrl.trim().length > 0)
36
+ return baseUrl.trim();
37
+ const host = req.headers.host ?? "localhost";
38
+ const protocol = req.protocol ?? "http";
39
+ return `${protocol}://${host}`;
40
+ }
41
+ /**
42
+ * Create a Fastify server with mock endpoints and docs.
43
+ */
44
+ function buildServer(spec, meta) {
45
+ const app = (0, fastify_1.default)({
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
+ }
90
+ });
91
+ /**
92
+ * Handler for the /__spec route with bound metadata.
93
+ */
94
+ function specRouteHandler() {
95
+ return specHandler(spec, meta);
96
+ }
97
+ /**
98
+ * Handler for the /__openapi.json route.
99
+ */
100
+ function openApiJsonRouteHandler(req) {
101
+ return openApiJsonHandler(req, spec, meta?.baseUrl);
102
+ }
103
+ /**
104
+ * Handler for the /__openapi.yaml route.
105
+ */
106
+ function openApiYamlRouteHandler(req, reply) {
107
+ return openApiYamlHandler(req, reply, spec, meta?.baseUrl);
108
+ }
109
+ /**
110
+ * Handler for the /docs route.
111
+ */
112
+ function docsRouteHandler(req, reply) {
113
+ return docsHandler(req, reply);
114
+ }
115
+ // Basic sanity route
116
+ app.get("/health", healthHandler);
117
+ // Internal inspection endpoint
118
+ app.get("/__spec", specRouteHandler);
119
+ app.get("/__openapi.json", openApiJsonRouteHandler);
120
+ app.get("/__openapi.yaml", openApiYamlRouteHandler);
121
+ app.register(static_1.default, {
122
+ root: getSwaggerUiRoot(),
123
+ prefix: "/docs/assets/",
124
+ decorateReply: false
125
+ });
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
+ });
142
+ (0, registerEndpoints_js_1.registerEndpoints)(app, spec);
143
+ return app;
144
+ }
145
+ /**
146
+ * Health check handler.
147
+ */
148
+ function healthHandler() {
149
+ return { ok: true };
150
+ }
151
+ /**
152
+ * Debug handler that exposes the current spec and metadata.
153
+ */
154
+ function specHandler(spec, meta) {
155
+ return {
156
+ meta: {
157
+ specPath: meta?.specPath ?? null,
158
+ loadedAt: meta?.loadedAt ?? null
159
+ },
160
+ spec
161
+ };
162
+ }
163
+ /**
164
+ * Serve the OpenAPI document as JSON.
165
+ */
166
+ function openApiJsonHandler(req, spec, baseUrl) {
167
+ const serverUrl = resolveServerUrl(req, baseUrl);
168
+ return (0, openapi_js_1.generateOpenApi)(spec, serverUrl);
169
+ }
170
+ /**
171
+ * Serve the OpenAPI document as YAML.
172
+ */
173
+ function openApiYamlHandler(req, reply, spec, baseUrl) {
174
+ const serverUrl = resolveServerUrl(req, baseUrl);
175
+ const doc = (0, openapi_js_1.generateOpenApi)(spec, serverUrl);
176
+ const yaml = yaml_1.default.stringify(doc);
177
+ reply.type("application/yaml; charset=utf-8").send(yaml);
178
+ }
179
+ /**
180
+ * Serve the Swagger UI HTML page.
181
+ */
182
+ function docsHandler(_req, reply) {
183
+ const html = `<!DOCTYPE html>
184
+ <html lang="en">
185
+ <head>
186
+ <meta charset="UTF-8" />
187
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
188
+ <title>mockserve docs</title>
189
+ <link rel="stylesheet" href="/docs/assets/swagger-ui.css" />
190
+ <style>
191
+ html, body { margin: 0; padding: 0; }
192
+ </style>
193
+ </head>
194
+ <body>
195
+ <div id="swagger-ui"></div>
196
+
197
+ <script src="/docs/assets/swagger-ui-bundle.js"></script>
198
+ <script src="/docs/assets/swagger-ui-standalone-preset.js"></script>
199
+ <script>
200
+ window.ui = SwaggerUIBundle({
201
+ url: "/__openapi.json",
202
+ dom_id: "#swagger-ui",
203
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
204
+ layout: "BaseLayout"
205
+ });
206
+ </script>
207
+ </body>
208
+ </html>`;
209
+ reply.type("text/html; charset=utf-8").send(html);
210
+ }
package/dist/spec.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MockSpecSchema = exports.EndpointSchema = exports.VariantSchema = exports.MatchSchema = void 0;
37
+ const z = __importStar(require("zod"));
38
+ const PrimitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
39
+ const TemplateValueSchema = z.lazy(() => z.union([
40
+ PrimitiveSchema,
41
+ z.null(),
42
+ z.array(TemplateValueSchema),
43
+ z.object({ __faker: z.string().min(1) }).strict(),
44
+ z
45
+ .object({
46
+ __faker: z
47
+ .object({
48
+ method: z.string().min(1),
49
+ args: z.array(TemplateValueSchema).optional()
50
+ })
51
+ .strict()
52
+ })
53
+ .strict(),
54
+ z
55
+ .object({
56
+ __repeat: z
57
+ .object({
58
+ min: z.number().int().min(0).optional(),
59
+ max: z.number().int().min(0).optional(),
60
+ count: z.number().int().min(0).optional(),
61
+ template: TemplateValueSchema
62
+ })
63
+ .strict()
64
+ })
65
+ .strict(),
66
+ z.record(z.string(), TemplateValueSchema)
67
+ ]));
68
+ exports.MatchSchema = z.object({
69
+ query: z.record(z.string(), PrimitiveSchema).optional(),
70
+ // Exact match for top-level body fields only (keeps v1 simple)
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()
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
+ ]);
84
+ exports.VariantSchema = z.object({
85
+ name: z.string().min(1).optional(),
86
+ match: exports.MatchSchema.optional(),
87
+ status: z.number().int().min(100).max(599).optional(),
88
+ response: TemplateValueSchema,
89
+ headers: z.record(z.string(), z.string()).optional(),
90
+ // Simulation overrides per variant (optional)
91
+ delayMs: z.number().int().min(0).optional(),
92
+ delay: DelaySchema.optional(),
93
+ errorRate: z.number().min(0).max(1).optional(),
94
+ errorStatus: z.number().int().min(100).max(599).optional(),
95
+ errorResponse: TemplateValueSchema.optional()
96
+ });
97
+ exports.EndpointSchema = z.object({
98
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']),
99
+ path: z.string().min(1),
100
+ match: exports.MatchSchema.optional(),
101
+ variants: z.array(exports.VariantSchema).min(1).optional(),
102
+ // Response behavior:
103
+ status: z.number().int().min(200).max(599).default(200),
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(),
117
+ // Simulation (optional overrides)
118
+ delayMs: z.number().int().min(0).optional(),
119
+ delay: DelaySchema.optional(),
120
+ errorRate: z.number().min(0).max(1).optional(),
121
+ errorStatus: z.number().int().min(100).max(599).optional(),
122
+ errorResponse: TemplateValueSchema.optional()
123
+ });
124
+ exports.MockSpecSchema = z.object({
125
+ version: z.literal(1),
126
+ settings: z
127
+ .object({
128
+ delayMs: z.number().int().min(0).default(0),
129
+ errorRate: z.number().min(0).max(1).default(0),
130
+ errorStatus: z.number().int().min(100).max(599).default(500),
131
+ errorResponse: TemplateValueSchema.default({ error: "Mock error" }),
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()
143
+ })
144
+ .default({ delayMs: 0, errorRate: 0, errorStatus: 500, errorResponse: { error: "Mock error" } }),
145
+ endpoints: z.array(exports.EndpointSchema).min(1)
146
+ });
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPathValue = getPathValue;
4
+ exports.renderStringTemplate = renderStringTemplate;
5
+ /**
6
+ * Safely read a nested property from an object using a dot path.
7
+ */
8
+ function getPathValue(obj, path) {
9
+ if (!path)
10
+ return undefined;
11
+ const parts = path.split(".");
12
+ let current = obj;
13
+ for (const part of parts) {
14
+ if (current === null || current === undefined)
15
+ return undefined;
16
+ if (typeof current !== "object")
17
+ return undefined;
18
+ const record = current;
19
+ current = record[part];
20
+ }
21
+ return current;
22
+ }
23
+ /**
24
+ * Render string templates like {{params.id}}, {{query.type}}, {{body.email}}.
25
+ */
26
+ function renderStringTemplate(input, ctx) {
27
+ /**
28
+ * Replace a single template token with its resolved value.
29
+ */
30
+ function replaceToken(_match, root, path) {
31
+ let source;
32
+ if (root === "params")
33
+ source = ctx.params;
34
+ else if (root === "query")
35
+ source = ctx.query;
36
+ else if (root === "body")
37
+ source = ctx.body;
38
+ else
39
+ return "";
40
+ const value = getPathValue(source, path);
41
+ if (value === undefined || value === null)
42
+ return "";
43
+ if (typeof value === "string")
44
+ return value;
45
+ if (typeof value === "number" || typeof value === "boolean")
46
+ return String(value);
47
+ try {
48
+ return JSON.stringify(value);
49
+ }
50
+ catch {
51
+ return "";
52
+ }
53
+ }
54
+ return input.replace(/\{\{\s*([a-zA-Z]+)\.([a-zA-Z0-9_.]+)\s*\}\}/g, replaceToken);
55
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "../mockserve.spec.schema.json",
3
+ "version": 1,
4
+ "settings": {
5
+ "delayMs": 100,
6
+ "errorRate": 0,
7
+ "errorStatus": 500,
8
+ "errorResponse": { "error": "Mock error" }
9
+ },
10
+ "endpoints": [
11
+ {
12
+ "method": "POST",
13
+ "path": "/login",
14
+ "response": { "ok": true, "token": "tok_{{body.email}}" },
15
+ "variants": [
16
+ {
17
+ "name": "invalid password",
18
+ "match": { "body": { "password": "wrong" } },
19
+ "status": 401,
20
+ "response": { "ok": false, "error": "Invalid credentials" }
21
+ },
22
+ {
23
+ "name": "admin login",
24
+ "match": { "body": { "email": "admin@example.com" } },
25
+ "status": 200,
26
+ "response": { "ok": true, "role": "admin", "token": "tok_admin" }
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "../mockserve.spec.schema.json",
3
+ "version": 1,
4
+ "settings": {
5
+ "delayMs": 0,
6
+ "errorRate": 0,
7
+ "errorStatus": 500,
8
+ "errorResponse": { "error": "Mock error" }
9
+ },
10
+ "endpoints": [
11
+ {
12
+ "method": "GET",
13
+ "path": "/users",
14
+ "response": {
15
+ "users": [
16
+ { "id": "u1", "name": "Ada Lovelace" },
17
+ { "id": "u2", "name": "Alan Turing" }
18
+ ]
19
+ }
20
+ },
21
+ {
22
+ "method": "GET",
23
+ "path": "/users/:id",
24
+ "response": {
25
+ "id": "{{params.id}}",
26
+ "message": "User fetched"
27
+ }
28
+ },
29
+ {
30
+ "method": "POST",
31
+ "path": "/users",
32
+ "status": 201,
33
+ "response": {
34
+ "id": "u_{{body.email}}",
35
+ "name": "{{body.name}}",
36
+ "created": true
37
+ }
38
+ },
39
+ {
40
+ "method": "DELETE",
41
+ "path": "/users/:id",
42
+ "status": 204,
43
+ "response": { "deleted": "{{params.id}}" }
44
+ }
45
+ ]
46
+ }