@zze/mock-server 0.2.5 → 0.5.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 (38) hide show
  1. package/README.md +117 -28
  2. package/dist/__tests__/mock-server.test.js +52 -0
  3. package/dist/admin/__tests__/admin-router.test.js +0 -151
  4. package/dist/admin/admin-router.d.ts.map +1 -1
  5. package/dist/admin/admin-router.js +0 -205
  6. package/dist/admin/types.d.ts +0 -84
  7. package/dist/admin/types.d.ts.map +1 -1
  8. package/dist/admin/types.js +1 -5
  9. package/dist/config/config-writer.d.ts +1 -35
  10. package/dist/config/config-writer.d.ts.map +1 -1
  11. package/dist/config/config-writer.js +0 -103
  12. package/dist/mock-server.d.ts +14 -3
  13. package/dist/mock-server.d.ts.map +1 -1
  14. package/dist/mock-server.js +57 -60
  15. package/dist/resolver/__tests__/response-resolver.test.js +134 -0
  16. package/dist/resolver/response-resolver.d.ts +18 -2
  17. package/dist/resolver/response-resolver.d.ts.map +1 -1
  18. package/dist/resolver/response-resolver.js +62 -5
  19. package/dist/resolver/types.d.ts +8 -0
  20. package/dist/resolver/types.d.ts.map +1 -1
  21. package/dist/resolver/types.js +7 -0
  22. package/dist/schemas/__tests__/endpoint.schema.test.js +48 -0
  23. package/dist/schemas/endpoint.schema.d.ts +31 -8
  24. package/dist/schemas/endpoint.schema.d.ts.map +1 -1
  25. package/dist/schemas/scenario.schema.d.ts +28 -4
  26. package/dist/schemas/scenario.schema.d.ts.map +1 -1
  27. package/dist/schemas/scenario.schema.js +23 -3
  28. package/dist/server/__tests__/mock-server-app.test.js +37 -0
  29. package/dist/server/mock-server-app.d.ts.map +1 -1
  30. package/dist/server/mock-server-app.js +5 -1
  31. package/dist/server/types.d.ts +8 -0
  32. package/dist/server/types.d.ts.map +1 -1
  33. package/dist/server/types.js +1 -0
  34. package/dist/ui/assets/{index-rj7m-XdG.css → index-C5BZSa2z.css} +1 -1
  35. package/dist/ui/assets/index-DY4CS66u.js +9 -0
  36. package/dist/ui/index.html +2 -2
  37. package/package.json +6 -4
  38. package/dist/ui/assets/index-SADMOgPE.js +0 -10
package/README.md CHANGED
@@ -8,6 +8,8 @@ A reusable Node.js mock server for simulating backend APIs during local microfro
8
8
  - 🔄 **Hot reload** - Automatic config reloading on file changes
9
9
  - 🎭 **Scenarios** - Define multiple response scenarios per endpoint
10
10
  - 🌊 **Flows** - Activate groups of scenarios to simulate user journeys
11
+ - 📁 **File download** - Serve binary files (PDFs, images, CSVs) from scenarios
12
+ - 📤 **File upload** - Accept `multipart/form-data` upload requests out of the box
11
13
  - 🎨 **Admin UI** - Visual dashboard at `/mock-admin` to control the server
12
14
  - 🔌 **REST API** - Programmatic control via admin endpoints
13
15
  - 📦 **Single package** - Everything bundled, no external dependencies needed
@@ -138,12 +140,15 @@ interface ScenarioConfig {
138
140
  id: string; // Unique within endpoint
139
141
  name: string; // Display name in admin UI
140
142
  status: number; // HTTP status code (100-599)
141
- body?: any; // Response body (JSON or function)
143
+ body?: any; // Response body (JSON or function) — mutually exclusive with file
144
+ file?: string; // File path relative to configPath — mutually exclusive with body
142
145
  headers?: Record<string, string>; // Custom response headers
143
146
  delay?: number; // Response delay in ms
144
147
  }
145
148
  ```
146
149
 
150
+ > **Note:** `body` and `file` are mutually exclusive. Setting both will cause a validation error at startup.
151
+
147
152
  ### Flow Config
148
153
 
149
154
  Flows activate multiple scenarios at once to simulate complex user journeys.
@@ -177,6 +182,107 @@ interface FlowConfig {
177
182
 
178
183
  ---
179
184
 
185
+ ## File Download
186
+
187
+ A scenario can serve a file instead of JSON by specifying a `file` path relative to `configPath`:
188
+
189
+ **mock-config/endpoints/reports.json**
190
+ ```json
191
+ {
192
+ "id": "get-report",
193
+ "path": "/reports/:id",
194
+ "method": "GET",
195
+ "defaultScenarioId": "pdf",
196
+ "scenarios": [
197
+ {
198
+ "id": "pdf",
199
+ "name": "PDF Report",
200
+ "status": 200,
201
+ "file": "./assets/sample-report.pdf",
202
+ "headers": { "Content-Disposition": "attachment; filename=report.pdf" }
203
+ },
204
+ {
205
+ "id": "not-found",
206
+ "name": "Not Found",
207
+ "status": 404,
208
+ "body": { "error": "Report not found" }
209
+ }
210
+ ]
211
+ }
212
+ ```
213
+
214
+ Place the file at `mock-config/assets/sample-report.pdf`. The `Content-Type` header is inferred automatically from the file extension if not set explicitly.
215
+
216
+ > `body` and `file` are mutually exclusive — setting both causes a validation error at startup.
217
+
218
+ ---
219
+
220
+ ## File Upload
221
+
222
+ Upload endpoints work out of the box — no special configuration needed. The server accepts `multipart/form-data` requests, discards the uploaded files, and returns the active scenario response:
223
+
224
+ **mock-config/endpoints/avatar.json**
225
+ ```json
226
+ {
227
+ "id": "upload-avatar",
228
+ "path": "/users/:id/avatar",
229
+ "method": "POST",
230
+ "defaultScenarioId": "success",
231
+ "scenarios": [
232
+ {
233
+ "id": "success",
234
+ "name": "Upload OK",
235
+ "status": 200,
236
+ "body": { "url": "/cdn/avatar.jpg" }
237
+ },
238
+ {
239
+ "id": "too-large",
240
+ "name": "File Too Large",
241
+ "status": 413,
242
+ "body": { "error": "File exceeds maximum size" }
243
+ }
244
+ ]
245
+ }
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Global Delay
251
+
252
+ Add a baseline latency to every response without modifying individual scenarios:
253
+
254
+ ```typescript
255
+ const server = new MockServer({
256
+ configPath: './mock-config',
257
+ globalDelay: 200, // every response waits 200 ms
258
+ });
259
+ ```
260
+
261
+ Or use a random range to simulate variable network conditions:
262
+
263
+ ```typescript
264
+ const server = new MockServer({
265
+ configPath: './mock-config',
266
+ globalDelay: [100, 500], // each response waits a random 100–500 ms
267
+ });
268
+ ```
269
+
270
+ **Priority:** a scenario's own `delay` field always takes precedence. `globalDelay` is only used when the resolved scenario has no `delay` set.
271
+
272
+ ```json
273
+ {
274
+ "id": "fast-scenario",
275
+ "name": "Always fast",
276
+ "status": 200,
277
+ "delay": 0,
278
+ "body": { "ok": true }
279
+ }
280
+ ```
281
+
282
+ In the example above, `delay: 0` overrides any `globalDelay` — the response is immediate.
283
+
284
+ ---
285
+
180
286
  ## TypeScript Configs
181
287
 
182
288
  You can write configs in TypeScript for type safety and dynamic responses:
@@ -218,9 +324,6 @@ export default endpoint;
218
324
  The built-in admin UI is available at `/mock-admin` and provides:
219
325
 
220
326
  - **Endpoints Panel**: View all endpoints, expand to see scenarios, click to activate
221
- - Create new endpoints with the JSON editor
222
- - Edit existing endpoint configurations
223
- - Delete endpoints with confirmation
224
327
  - **Flows Panel**: View and activate user flows with category grouping
225
328
  - Create new flows with the endpoint/scenario picker
226
329
  - Edit or duplicate existing flows
@@ -248,13 +351,6 @@ Control the mock server programmatically:
248
351
  | POST | `/mock-admin/api/flows/activate` | Activate/deactivate a flow |
249
352
  | POST | `/mock-admin/api/reset` | Reset to default state |
250
353
 
251
- ### Endpoint CRUD
252
-
253
- | Method | Endpoint | Description |
254
- |--------|----------|-------------|
255
- | POST | `/mock-admin/api/endpoints` | Create a new endpoint |
256
- | PUT | `/mock-admin/api/endpoints/:id` | Update an existing endpoint |
257
- | DELETE | `/mock-admin/api/endpoints/:id` | Delete an endpoint |
258
354
 
259
355
  ### Flow CRUD
260
356
 
@@ -289,19 +385,6 @@ curl -X POST http://localhost:3001/mock-admin/api/flows/activate \
289
385
  # Reset to defaults
290
386
  curl -X POST http://localhost:3001/mock-admin/api/reset
291
387
 
292
- # Create a new endpoint
293
- curl -X POST http://localhost:3001/mock-admin/api/endpoints \
294
- -H "Content-Type: application/json" \
295
- -d '{"endpoint": {"id": "new-endpoint", "method": "GET", "path": "/api/test", "scenarios": [{"id": "default", "name": "Default", "response": {"status": 200, "body": {}}}]}}'
296
-
297
- # Update an endpoint
298
- curl -X PUT http://localhost:3001/mock-admin/api/endpoints/new-endpoint \
299
- -H "Content-Type: application/json" \
300
- -d '{"endpoint": {"id": "new-endpoint", "method": "GET", "path": "/api/test-updated", "scenarios": [{"id": "default", "name": "Default", "response": {"status": 200, "body": {"updated": true}}}]}}'
301
-
302
- # Delete an endpoint
303
- curl -X DELETE http://localhost:3001/mock-admin/api/endpoints/new-endpoint
304
-
305
388
  # Create a new flow
306
389
  curl -X POST http://localhost:3001/mock-admin/api/flows \
307
390
  -H "Content-Type: application/json" \
@@ -328,13 +411,13 @@ curl -X DELETE http://localhost:3001/mock-admin/api/flows/my-flow
328
411
  ### MockServer Class
329
412
 
330
413
  ```typescript
331
- import { MockServer } from '@zze/mock-server';
332
-
333
414
  const server = new MockServer({
334
- configPath: './mock-config', // Required
415
+ configPath: './mock-config',
335
416
  port: 3001, // Default: 3001
336
417
  apiPrefix: '/api', // Default: '/api'
337
- hotReload: true // Default: true
418
+ hotReload: true, // Default: true
419
+ globalDelay: 200 // Optional: add 200 ms to every response (unless scenario overrides)
420
+ // globalDelay: [100, 500] // Or a random range in ms
338
421
  });
339
422
  ```
340
423
 
@@ -592,6 +675,12 @@ interface MockServerOptions {
592
675
  apiPrefix?: string;
593
676
  hotReload?: boolean;
594
677
  watcherOptions?: WatcherOptions;
678
+ /**
679
+ * Global delay fallback for all responses.
680
+ * Scenario-level `delay` always overrides this.
681
+ * A number is a fixed ms value; a tuple is a [min, max] random range.
682
+ */
683
+ globalDelay?: number | [number, number];
595
684
  }
596
685
 
597
686
  // Server state
@@ -596,4 +596,56 @@ const supertest_1 = __importDefault(require("supertest"));
596
596
  (0, vitest_1.expect)(mockResponse.status).toBe(500);
597
597
  });
598
598
  });
599
+ (0, vitest_1.describe)('globalDelay option', () => {
600
+ let server;
601
+ (0, vitest_1.afterEach)(async () => {
602
+ if (server?.getState().running) {
603
+ await server.stop();
604
+ }
605
+ });
606
+ (0, vitest_1.it)('should apply static globalDelay to responses', async () => {
607
+ await createEndpoint('delay-ep', '/delay-ep', 'GET', [
608
+ { id: 'default', name: 'Default', status: 200, body: { ok: true }, isDefault: true },
609
+ ]);
610
+ server = new mock_server_js_1.MockServer({
611
+ configPath: configDir,
612
+ port: 3040,
613
+ hotReload: false,
614
+ globalDelay: 80,
615
+ });
616
+ await server.start();
617
+ const app = server.getApp();
618
+ const start = Date.now();
619
+ const res = await (0, supertest_1.default)(app).get('/api/delay-ep');
620
+ const elapsed = Date.now() - start;
621
+ (0, vitest_1.expect)(res.status).toBe(200);
622
+ (0, vitest_1.expect)(elapsed).toBeGreaterThanOrEqual(70); // 80 ms with small margin
623
+ });
624
+ (0, vitest_1.it)('scenario-level delay should take precedence over globalDelay', async () => {
625
+ await createEndpoint('prio-ep', '/prio-ep', 'GET', [
626
+ {
627
+ id: 'fast',
628
+ name: 'Fast',
629
+ status: 200,
630
+ body: { ok: true },
631
+ // @ts-ignore - delay is valid in EndpointConfig scenarios
632
+ delay: 0,
633
+ isDefault: true,
634
+ },
635
+ ]);
636
+ server = new mock_server_js_1.MockServer({
637
+ configPath: configDir,
638
+ port: 3041,
639
+ hotReload: false,
640
+ globalDelay: 5000, // would time out the test if used
641
+ });
642
+ await server.start();
643
+ const app = server.getApp();
644
+ const start = Date.now();
645
+ const res = await (0, supertest_1.default)(app).get('/api/prio-ep');
646
+ const elapsed = Date.now() - start;
647
+ (0, vitest_1.expect)(res.status).toBe(200);
648
+ (0, vitest_1.expect)(elapsed).toBeLessThan(300); // scenario delay=0 wins, should be fast
649
+ });
650
+ });
599
651
  });
@@ -281,131 +281,6 @@ const mockFlows = [
281
281
  .expect(200);
282
282
  });
283
283
  });
284
- (0, vitest_1.describe)('Endpoint CRUD operations', () => {
285
- let mockConfigWriter;
286
- let onConfigChangeCalled;
287
- (0, vitest_1.beforeEach)(() => {
288
- onConfigChangeCalled = false;
289
- mockConfigWriter = {
290
- writeEndpoint: vitest_1.vi.fn().mockResolvedValue({ success: true, filePath: '/test/endpoint.json' }),
291
- writeFlow: vitest_1.vi.fn().mockResolvedValue({ success: true, filePath: '/test/flow.json' }),
292
- deleteEndpoint: vitest_1.vi.fn().mockResolvedValue({ success: true }),
293
- deleteFlow: vitest_1.vi.fn().mockResolvedValue({ success: true }),
294
- getEndpointFilePath: vitest_1.vi.fn().mockReturnValue('/test/endpoint.json'),
295
- getFlowFilePath: vitest_1.vi.fn().mockReturnValue('/test/flow.json'),
296
- findEndpointFile: vitest_1.vi.fn().mockResolvedValue(null),
297
- findFlowFile: vitest_1.vi.fn().mockResolvedValue(null),
298
- };
299
- serverApp.setConfigWriter(mockConfigWriter, async () => {
300
- onConfigChangeCalled = true;
301
- });
302
- });
303
- (0, vitest_1.describe)('POST /mock-admin/api/endpoints', () => {
304
- (0, vitest_1.it)('should create a new endpoint', async () => {
305
- const newEndpoint = {
306
- id: 'new-endpoint',
307
- path: '/new',
308
- method: 'POST',
309
- scenarios: [
310
- { id: 'success', name: 'Success', status: 201, body: { created: true } },
311
- ],
312
- defaultScenarioId: 'success',
313
- };
314
- const res = await (0, supertest_1.default)(serverApp.getApp())
315
- .post('/mock-admin/api/endpoints')
316
- .send(newEndpoint)
317
- .expect(201);
318
- (0, vitest_1.expect)(res.body.id).toBe('new-endpoint');
319
- (0, vitest_1.expect)(res.body.filePath).toBeDefined();
320
- (0, vitest_1.expect)(mockConfigWriter.writeEndpoint).toHaveBeenCalledWith(newEndpoint);
321
- (0, vitest_1.expect)(onConfigChangeCalled).toBe(true);
322
- });
323
- (0, vitest_1.it)('should return 409 if endpoint already exists', async () => {
324
- const existingEndpoint = {
325
- id: 'get-users',
326
- path: '/users',
327
- method: 'GET',
328
- scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
329
- defaultScenarioId: 's',
330
- };
331
- const res = await (0, supertest_1.default)(serverApp.getApp())
332
- .post('/mock-admin/api/endpoints')
333
- .send(existingEndpoint)
334
- .expect(409);
335
- (0, vitest_1.expect)(res.body.error).toContain('already exists');
336
- });
337
- (0, vitest_1.it)('should return 400 for invalid endpoint', async () => {
338
- const res = await (0, supertest_1.default)(serverApp.getApp())
339
- .post('/mock-admin/api/endpoints')
340
- .send({ id: 'test' }) // Missing required fields
341
- .expect(400);
342
- (0, vitest_1.expect)(res.body.error).toBe('Invalid request body');
343
- });
344
- });
345
- (0, vitest_1.describe)('PUT /mock-admin/api/endpoints/:id', () => {
346
- (0, vitest_1.it)('should update an existing endpoint', async () => {
347
- const updatedEndpoint = {
348
- id: 'get-users',
349
- path: '/users/updated',
350
- method: 'GET',
351
- scenarios: [
352
- { id: 'success', name: 'Success', status: 200, body: { updated: true } },
353
- ],
354
- defaultScenarioId: 'success',
355
- };
356
- const res = await (0, supertest_1.default)(serverApp.getApp())
357
- .put('/mock-admin/api/endpoints/get-users')
358
- .send(updatedEndpoint)
359
- .expect(200);
360
- (0, vitest_1.expect)(res.body.id).toBe('get-users');
361
- (0, vitest_1.expect)(mockConfigWriter.writeEndpoint).toHaveBeenCalledWith(updatedEndpoint);
362
- (0, vitest_1.expect)(onConfigChangeCalled).toBe(true);
363
- });
364
- (0, vitest_1.it)('should return 404 if endpoint not found', async () => {
365
- const endpoint = {
366
- id: 'nonexistent',
367
- path: '/test',
368
- method: 'GET',
369
- scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
370
- defaultScenarioId: 's',
371
- };
372
- const res = await (0, supertest_1.default)(serverApp.getApp())
373
- .put('/mock-admin/api/endpoints/nonexistent')
374
- .send(endpoint)
375
- .expect(404);
376
- (0, vitest_1.expect)(res.body.error).toContain('not found');
377
- });
378
- (0, vitest_1.it)('should return 400 if ID in body does not match URL', async () => {
379
- const endpoint = {
380
- id: 'different-id',
381
- path: '/users',
382
- method: 'GET',
383
- scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
384
- defaultScenarioId: 's',
385
- };
386
- const res = await (0, supertest_1.default)(serverApp.getApp())
387
- .put('/mock-admin/api/endpoints/get-users')
388
- .send(endpoint)
389
- .expect(400);
390
- (0, vitest_1.expect)(res.body.error).toContain('must match');
391
- });
392
- });
393
- (0, vitest_1.describe)('DELETE /mock-admin/api/endpoints/:id', () => {
394
- (0, vitest_1.it)('should delete an existing endpoint', async () => {
395
- await (0, supertest_1.default)(serverApp.getApp())
396
- .delete('/mock-admin/api/endpoints/get-users')
397
- .expect(204);
398
- (0, vitest_1.expect)(mockConfigWriter.deleteEndpoint).toHaveBeenCalledWith('get-users');
399
- (0, vitest_1.expect)(onConfigChangeCalled).toBe(true);
400
- });
401
- (0, vitest_1.it)('should return 404 if endpoint not found', async () => {
402
- const res = await (0, supertest_1.default)(serverApp.getApp())
403
- .delete('/mock-admin/api/endpoints/nonexistent')
404
- .expect(404);
405
- (0, vitest_1.expect)(res.body.error).toContain('not found');
406
- });
407
- });
408
- });
409
284
  (0, vitest_1.describe)('Flow CRUD operations', () => {
410
285
  let mockConfigWriter;
411
286
  let onConfigChangeCalled;
@@ -561,32 +436,6 @@ const mockFlows = [
561
436
  });
562
437
  });
563
438
  (0, vitest_1.describe)('CRUD operations without ConfigWriter', () => {
564
- (0, vitest_1.it)('should return 501 for endpoint CRUD when ConfigWriter not configured', async () => {
565
- const endpoint = {
566
- id: 'test',
567
- path: '/test',
568
- method: 'GET',
569
- scenarios: [{ id: 's', name: 'S', status: 200, body: {} }],
570
- defaultScenarioId: 's',
571
- };
572
- // POST
573
- let res = await (0, supertest_1.default)(serverApp.getApp())
574
- .post('/mock-admin/api/endpoints')
575
- .send(endpoint)
576
- .expect(501);
577
- (0, vitest_1.expect)(res.body.error).toContain('not available');
578
- // PUT
579
- res = await (0, supertest_1.default)(serverApp.getApp())
580
- .put('/mock-admin/api/endpoints/get-users')
581
- .send({ ...endpoint, id: 'get-users' })
582
- .expect(501);
583
- (0, vitest_1.expect)(res.body.error).toContain('not available');
584
- // DELETE
585
- res = await (0, supertest_1.default)(serverApp.getApp())
586
- .delete('/mock-admin/api/endpoints/get-users')
587
- .expect(501);
588
- (0, vitest_1.expect)(res.body.error).toContain('not available');
589
- });
590
439
  (0, vitest_1.it)('should return 501 for flow CRUD when ConfigWriter not configured', async () => {
591
440
  const flow = {
592
441
  id: 'test',
@@ -1 +1 @@
1
- {"version":3,"file":"admin-router.d.ts","sourceRoot":"","sources":["../../src/admin/admin-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAmC,MAAM,SAAS,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAYvD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,cAAc,EAAE,CAAC;IACrC,QAAQ,EAAE,MAAM,UAAU,EAAE,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,YAAY,GAAG,SAAS,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAAC;AACvE;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,cAAc,EAAE,EACpC,QAAQ,EAAE,MAAM,UAAU,EAAE,EAC5B,YAAY,EAAE,YAAY,GACzB,MAAM,CAAC;AAkrBV;;GAEG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"admin-router.d.ts","sourceRoot":"","sources":["../../src/admin/admin-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAmC,MAAM,SAAS,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWvD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,cAAc,EAAE,CAAC;IACrC,QAAQ,EAAE,MAAM,UAAU,EAAE,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,YAAY,GAAG,SAAS,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAAC;AACvE;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,cAAc,EAAE,EACpC,QAAQ,EAAE,MAAM,UAAU,EAAE,EAC5B,YAAY,EAAE,YAAY,GACzB,MAAM,CAAC;AA0cV;;GAEG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC"}
@@ -140,211 +140,6 @@ function createAdminRouter(optionsOrGetEndpoints, getFlowsArg, stateManagerArg)
140
140
  res.status(204).end();
141
141
  });
142
142
  // ============================================
143
- // Endpoint CRUD Operations
144
- // ============================================
145
- /**
146
- * GET /endpoints/:id
147
- * Get a single endpoint's full configuration (including scenario bodies)
148
- */
149
- router.get('/endpoints/:id', (req, res) => {
150
- const { id } = req.params;
151
- const endpoints = getEndpoints();
152
- const endpoint = endpoints.find((e) => e.id === id);
153
- if (!endpoint) {
154
- const errorResponse = {
155
- error: 'Endpoint not found',
156
- details: `No endpoint with id "${id}"`,
157
- };
158
- res.status(404).json(errorResponse);
159
- return;
160
- }
161
- res.json({
162
- id: endpoint.id,
163
- path: endpoint.path,
164
- method: endpoint.method,
165
- defaultScenarioId: endpoint.defaultScenarioId,
166
- scenarios: endpoint.scenarios.map((s) => ({
167
- id: s.id,
168
- name: s.name,
169
- status: s.status,
170
- body: s.body,
171
- headers: s.headers,
172
- delay: s.delay,
173
- })),
174
- });
175
- });
176
- /**
177
- * POST /endpoints
178
- * Create a new endpoint
179
- */
180
- router.post('/endpoints', async (req, res) => {
181
- const configWriter = getConfigWriter?.();
182
- if (!configWriter) {
183
- const errorResponse = {
184
- error: 'Endpoint management not available',
185
- details: 'ConfigWriter not configured',
186
- };
187
- res.status(501).json(errorResponse);
188
- return;
189
- }
190
- const parseResult = types_js_1.EndpointRequestSchema.safeParse(req.body);
191
- if (!parseResult.success) {
192
- const errorResponse = {
193
- error: 'Invalid request body',
194
- details: parseResult.error.issues,
195
- };
196
- res.status(400).json(errorResponse);
197
- return;
198
- }
199
- const endpoint = parseResult.data;
200
- // Check if endpoint with this ID already exists
201
- const existingEndpoint = getEndpoints().find((e) => e.id === endpoint.id);
202
- if (existingEndpoint) {
203
- const errorResponse = {
204
- error: `Endpoint with id '${endpoint.id}' already exists`,
205
- };
206
- res.status(409).json(errorResponse);
207
- return;
208
- }
209
- try {
210
- const result = await configWriter.writeEndpoint(endpoint);
211
- if (!result.success) {
212
- const errorResponse = {
213
- error: 'Failed to create endpoint',
214
- details: result.error,
215
- };
216
- res.status(500).json(errorResponse);
217
- return;
218
- }
219
- // Trigger config reload if available
220
- const onConfigChange = getOnConfigChange?.();
221
- if (onConfigChange) {
222
- await onConfigChange();
223
- }
224
- res.status(201).json({ id: endpoint.id, filePath: result.filePath });
225
- }
226
- catch (error) {
227
- const errorResponse = {
228
- error: error instanceof Error ? error.message : 'Unknown error',
229
- };
230
- res.status(500).json(errorResponse);
231
- }
232
- });
233
- /**
234
- * PUT /endpoints/:id
235
- * Update an existing endpoint
236
- */
237
- router.put('/endpoints/:id', async (req, res) => {
238
- const configWriter = getConfigWriter?.();
239
- if (!configWriter) {
240
- const errorResponse = {
241
- error: 'Endpoint management not available',
242
- details: 'ConfigWriter not configured',
243
- };
244
- res.status(501).json(errorResponse);
245
- return;
246
- }
247
- const { id } = req.params;
248
- const parseResult = types_js_1.EndpointRequestSchema.safeParse(req.body);
249
- if (!parseResult.success) {
250
- const errorResponse = {
251
- error: 'Invalid request body',
252
- details: parseResult.error.issues,
253
- };
254
- res.status(400).json(errorResponse);
255
- return;
256
- }
257
- const endpoint = parseResult.data;
258
- // Ensure the ID in the body matches the URL parameter
259
- if (endpoint.id !== id) {
260
- const errorResponse = {
261
- error: 'Endpoint ID in body must match URL parameter',
262
- };
263
- res.status(400).json(errorResponse);
264
- return;
265
- }
266
- // Check if endpoint exists
267
- const existingEndpoint = getEndpoints().find((e) => e.id === id);
268
- if (!existingEndpoint) {
269
- const errorResponse = {
270
- error: `Endpoint with id '${id}' not found`,
271
- };
272
- res.status(404).json(errorResponse);
273
- return;
274
- }
275
- try {
276
- const result = await configWriter.writeEndpoint(endpoint);
277
- if (!result.success) {
278
- const errorResponse = {
279
- error: 'Failed to update endpoint',
280
- details: result.error,
281
- };
282
- res.status(500).json(errorResponse);
283
- return;
284
- }
285
- // Trigger config reload if available
286
- const onConfigChange = getOnConfigChange?.();
287
- if (onConfigChange) {
288
- await onConfigChange();
289
- }
290
- res.status(200).json({ id: endpoint.id, filePath: result.filePath });
291
- }
292
- catch (error) {
293
- const errorResponse = {
294
- error: error instanceof Error ? error.message : 'Unknown error',
295
- };
296
- res.status(500).json(errorResponse);
297
- }
298
- });
299
- /**
300
- * DELETE /endpoints/:id
301
- * Delete an endpoint
302
- */
303
- router.delete('/endpoints/:id', async (req, res) => {
304
- const configWriter = getConfigWriter?.();
305
- if (!configWriter) {
306
- const errorResponse = {
307
- error: 'Endpoint management not available',
308
- details: 'ConfigWriter not configured',
309
- };
310
- res.status(501).json(errorResponse);
311
- return;
312
- }
313
- const { id } = req.params;
314
- // Check if endpoint exists
315
- const existingEndpoint = getEndpoints().find((e) => e.id === id);
316
- if (!existingEndpoint) {
317
- const errorResponse = {
318
- error: `Endpoint with id '${id}' not found`,
319
- };
320
- res.status(404).json(errorResponse);
321
- return;
322
- }
323
- try {
324
- const result = await configWriter.deleteEndpoint(id);
325
- if (!result.success) {
326
- const errorResponse = {
327
- error: 'Failed to delete endpoint',
328
- details: result.error,
329
- };
330
- res.status(500).json(errorResponse);
331
- return;
332
- }
333
- // Trigger config reload if available
334
- const onConfigChange = getOnConfigChange?.();
335
- if (onConfigChange) {
336
- await onConfigChange();
337
- }
338
- res.status(204).end();
339
- }
340
- catch (error) {
341
- const errorResponse = {
342
- error: error instanceof Error ? error.message : 'Unknown error',
343
- };
344
- res.status(500).json(errorResponse);
345
- }
346
- });
347
- // ============================================
348
143
  // Flow CRUD Operations
349
144
  // ============================================
350
145
  /**