@yrest/cli 0.6.0 → 0.7.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/README.md CHANGED
@@ -56,6 +56,7 @@ A YAML-first alternative to json-server for frontend development.
56
56
  | Custom static routes (`_routes`) | ✅ | ❌ |
57
57
  | Template variables in responses | ✅ | ❌ |
58
58
  | Handler functions (JS logic) | ✅ | ❌ |
59
+ | Conditional scenarios (`scenarios:`) | ✅ | ❌ |
59
60
  | Snapshot endpoints | ✅ | ❌ |
60
61
  | Config file | ✅ | ⚠️ |
61
62
  | API overview page (`/_about`) | ✅ | ❌ |
@@ -497,6 +498,103 @@ Available variables:
497
498
 
498
499
  When a field contains only a single `{{variable}}` placeholder, the resolved value preserves its original type (number, boolean, object). When embedded in a larger string it is stringified.
499
500
 
501
+ ### Conditional scenarios
502
+
503
+ Define multiple conditional response variants for a custom route. Scenarios are evaluated in declaration order — the first matching `when:` wins. If none match, the `otherwise:` block is used (if defined), otherwise the static `response:` block.
504
+
505
+ ```yaml
506
+ _routes:
507
+ - method: POST
508
+ path: /login
509
+ scenarios:
510
+ - when:
511
+ body.email: ana@test.com
512
+ body.password: secret
513
+ response:
514
+ status: 200
515
+ body:
516
+ token: tok-ana
517
+ - when:
518
+ body.email: admin@test.com
519
+ body.password: admin
520
+ response:
521
+ status: 200
522
+ body:
523
+ token: tok-admin
524
+ role: admin
525
+ otherwise:
526
+ status: 401
527
+ body:
528
+ error: Invalid credentials
529
+ ```
530
+
531
+ **`when:` as an object** — all entries must match (AND semantics):
532
+
533
+ ```yaml
534
+ when:
535
+ body.email: ana@test.com
536
+ body.password: secret
537
+ ```
538
+
539
+ **`when:` as an array of objects** — any group satisfying all its conditions matches (OR of ANDs):
540
+
541
+ ```yaml
542
+ when:
543
+ - body.role: admin
544
+ - body.role: superadmin
545
+ ```
546
+
547
+ Condition keys use dot-notation to address request data:
548
+
549
+ | Prefix | Example | Resolves to |
550
+ | ----------- | ------------------- | -------------------------- |
551
+ | `body.X` | `body.email` | `req.body.email` |
552
+ | `params.X` | `params.id` | `req.params.id` |
553
+ | `query.X` | `query.page` | `req.query.page` |
554
+ | `headers.X` | `headers.x-api-key` | `req.headers["x-api-key"]` |
555
+
556
+ Field operator suffixes (`_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`) work on condition keys exactly as they do on query params:
557
+
558
+ ```yaml
559
+ scenarios:
560
+ - when:
561
+ body.name_like: ana # name contains "ana" (case-insensitive)
562
+ body.age_gte: "18" # age >= 18
563
+ response:
564
+ status: 200
565
+ body: { ok: true }
566
+ ```
567
+
568
+ Template variables (`{{}}`) are supported in both scenario and `otherwise` response bodies:
569
+
570
+ ```yaml
571
+ scenarios:
572
+ - when:
573
+ body.email: ana@test.com
574
+ response:
575
+ status: 200
576
+ body:
577
+ message: "Welcome {{body.email}}"
578
+ otherwise:
579
+ status: 401
580
+ body:
581
+ error: "Unknown user: {{body.email}}"
582
+ ```
583
+
584
+ ### Per-route delay
585
+
586
+ Add a fixed delay (ms) to a specific route without affecting the rest of the server. Takes priority over the global `--delay` option for that route:
587
+
588
+ ```yaml
589
+ _routes:
590
+ - method: GET
591
+ path: /slow-endpoint
592
+ delay: 800
593
+ response:
594
+ status: 200
595
+ body: { data: loaded }
596
+ ```
597
+
500
598
  ### Handler functions
501
599
 
502
600
  For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the `handler:` field:
@@ -844,23 +942,24 @@ const server = createYrestServer({
844
942
 
845
943
  ## Roadmap
846
944
 
847
- | Feature | Status |
848
- | ------------------------------------------------- | ------ |
849
- | Full CRUD from `db.yml` | ✅ |
850
- | Field filters, operators, full-text search | ✅ |
851
- | Relations, `_expand`, `_embed`, nested routes | ✅ |
852
- | Pagination, sorting, field projection | ✅ |
853
- | Watch, readonly, delay, snapshot modes | ✅ |
854
- | Custom routes (`_routes`) with static responses | ✅ |
855
- | Template variables in responses (`{{params.id}}`) | ✅ |
856
- | Handler functions (`yrest.handlers.js`) | ✅ |
857
- | Visual panel (`/_panel`) | 🔜 |
858
- | Programmatic API for Vitest / Playwright | ✅ |
859
- | Docker image | 🔜 |
860
- | OpenAPI export (`yrest openapi db.yml`) | 🔜 |
861
- | VS Code extension with YAML snippets | 🔜 |
862
- | Request validation with JSON Schema | 🔜 |
863
- | Conditional scenarios | 🔜 |
945
+ | Feature | Status |
946
+ | -------------------------------------------------- | ------ |
947
+ | Full CRUD from `db.yml` | ✅ |
948
+ | Field filters, operators, full-text search | ✅ |
949
+ | Relations, `_expand`, `_embed`, nested routes | ✅ |
950
+ | Pagination, sorting, field projection | ✅ |
951
+ | Watch, readonly, delay, snapshot modes | ✅ |
952
+ | Custom routes (`_routes`) with static responses | ✅ |
953
+ | Template variables in responses (`{{params.id}}`) | ✅ |
954
+ | Handler functions (`yrest.handlers.js`) | ✅ |
955
+ | Visual panel (`/_panel`) | 🔜 |
956
+ | Programmatic API for Vitest / Playwright | ✅ |
957
+ | Docker image | 🔜 |
958
+ | OpenAPI export (`yrest openapi db.yml`) | 🔜 |
959
+ | VS Code extension with YAML snippets | 🔜 |
960
+ | Request validation with JSON Schema | 🔜 |
961
+ | Conditional scenarios (`scenarios:`, `otherwise:`) | |
962
+ | Per-route delay (`delay:`) | ✅ |
864
963
 
865
964
  ---
866
965
 
package/dist/cli/index.js CHANGED
@@ -469,16 +469,33 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
469
469
  <table><tbody>
470
470
  ${customRoutes.map((r) => {
471
471
  const fullPath = `${base}${r.path}`;
472
+ const tags = [];
473
+ if (r.delay && r.delay > 0) {
474
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
475
+ }
476
+ if (r.scenarios?.length) {
477
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
478
+ tags.push(
479
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
480
+ );
481
+ }
482
+ if (r.otherwise) {
483
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
484
+ }
472
485
  let desc;
473
486
  if (r.handler) {
474
487
  const found = handlers.has(r.handler);
475
488
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
489
+ } else if (r.scenarios?.length) {
490
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
491
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
476
492
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
477
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
493
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
478
494
  } else {
479
495
  const status = r.response?.status ?? 200;
480
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
496
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
481
497
  }
498
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
482
499
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
483
500
  }).join("")}
484
501
  </tbody></table>
@@ -630,7 +647,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
630
647
 
631
648
  <div class="banner">
632
649
  <div class="banner-inner">
633
- <h1><span class="y">y</span><span class="rest">rest</span></h1>
650
+ <h1><span class="y">y</span><span class="rest">Rest</span></h1>
634
651
  <p>Zero-config REST API mock server</p>
635
652
  <div class="banner-meta">
636
653
  <span>URL <strong>${host}</strong></span>
@@ -682,7 +699,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
682
699
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
683
700
 
684
701
  <footer>
685
- Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
702
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
686
703
  </footer>
687
704
 
688
705
  </div>
@@ -987,7 +1004,63 @@ var CollectionRouteCommand = class {
987
1004
  }
988
1005
  };
989
1006
 
1007
+ // src/utils/conditions.ts
1008
+ function resolveRequestPath(dotPath, req) {
1009
+ const [root, ...rest] = dotPath.split(".");
1010
+ let value;
1011
+ switch (root) {
1012
+ case "body":
1013
+ value = req.body;
1014
+ break;
1015
+ case "params":
1016
+ value = req.params;
1017
+ break;
1018
+ case "query":
1019
+ value = req.query;
1020
+ break;
1021
+ case "headers":
1022
+ value = req.headers;
1023
+ break;
1024
+ default:
1025
+ return void 0;
1026
+ }
1027
+ for (const key of rest) {
1028
+ if (value != null && typeof value === "object") {
1029
+ value = value[key];
1030
+ } else {
1031
+ return void 0;
1032
+ }
1033
+ }
1034
+ return value;
1035
+ }
1036
+ function matchConditionGroup(group, req) {
1037
+ return Object.entries(group).every(([key, expected]) => {
1038
+ const op = OPERATORS.find((o) => key.endsWith(o));
1039
+ if (op) {
1040
+ const path = key.slice(0, -op.length);
1041
+ const value2 = resolveRequestPath(path, req);
1042
+ if (value2 === void 0) return false;
1043
+ return applyOperator(value2, op, String(expected));
1044
+ }
1045
+ const value = resolveRequestPath(key, req);
1046
+ return String(value) === String(expected);
1047
+ });
1048
+ }
1049
+ function matchWhen(when, req) {
1050
+ if (Array.isArray(when)) {
1051
+ return when.some((group) => matchConditionGroup(group, req));
1052
+ }
1053
+ return matchConditionGroup(when, req);
1054
+ }
1055
+ function findMatchingScenario(scenarios, req) {
1056
+ return scenarios.find((s) => matchWhen(s.when, req));
1057
+ }
1058
+
990
1059
  // src/router/routes/custom.routes.ts
1060
+ function resolveBody(body, ctx) {
1061
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1062
+ return body ?? null;
1063
+ }
991
1064
  var CustomRouteCommand = class {
992
1065
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
993
1066
  this.storage = storage;
@@ -1012,6 +1085,9 @@ var CustomRouteCommand = class {
1012
1085
  method,
1013
1086
  url,
1014
1087
  handler: async (req, reply) => {
1088
+ if (route.delay && route.delay > 0) {
1089
+ await new Promise((resolve5) => setTimeout(resolve5, route.delay));
1090
+ }
1015
1091
  for (const [key, value] of Object.entries(headers)) {
1016
1092
  reply.header(key, value);
1017
1093
  }
@@ -1021,13 +1097,13 @@ var CustomRouteCommand = class {
1021
1097
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1022
1098
  }
1023
1099
  try {
1024
- const ctx = {
1100
+ const ctx2 = {
1025
1101
  params: req.params,
1026
1102
  query: req.query,
1027
1103
  body: req.body,
1028
1104
  headers: req.headers
1029
1105
  };
1030
- const result = await fn(ctx);
1106
+ const result = await fn(ctx2);
1031
1107
  const resStatus = result.status ?? 200;
1032
1108
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1033
1109
  reply.header(k, v);
@@ -1039,12 +1115,24 @@ var CustomRouteCommand = class {
1039
1115
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1040
1116
  }
1041
1117
  }
1042
- const body = dynamic ? interpolate(rawBody, {
1118
+ const ctx = {
1043
1119
  params: req.params,
1044
1120
  query: req.query,
1045
1121
  body: req.body,
1046
1122
  headers: req.headers
1047
- }) : rawBody;
1123
+ };
1124
+ if (route.scenarios?.length) {
1125
+ const matched = findMatchingScenario(route.scenarios, ctx);
1126
+ const active = matched?.response ?? route.otherwise;
1127
+ if (active) {
1128
+ const aStatus = active.status ?? 200;
1129
+ const aBody = resolveBody(active.body, ctx);
1130
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1131
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1132
+ return reply.status(aStatus).send(aBody);
1133
+ }
1134
+ }
1135
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1048
1136
  if (body === null && status === 204) return reply.status(status).send();
1049
1137
  return reply.status(status).send(body);
1050
1138
  }
@@ -442,16 +442,33 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
442
442
  <table><tbody>
443
443
  ${customRoutes.map((r) => {
444
444
  const fullPath = `${base}${r.path}`;
445
+ const tags = [];
446
+ if (r.delay && r.delay > 0) {
447
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
448
+ }
449
+ if (r.scenarios?.length) {
450
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
451
+ tags.push(
452
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
453
+ );
454
+ }
455
+ if (r.otherwise) {
456
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
457
+ }
445
458
  let desc;
446
459
  if (r.handler) {
447
460
  const found = handlers.has(r.handler);
448
461
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
462
+ } else if (r.scenarios?.length) {
463
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
464
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
449
465
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
450
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
466
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
451
467
  } else {
452
468
  const status = r.response?.status ?? 200;
453
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
469
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
454
470
  }
471
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
455
472
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
456
473
  }).join("")}
457
474
  </tbody></table>
@@ -603,7 +620,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
603
620
 
604
621
  <div class="banner">
605
622
  <div class="banner-inner">
606
- <h1><span class="y">y</span><span class="rest">rest</span></h1>
623
+ <h1><span class="y">y</span><span class="rest">Rest</span></h1>
607
624
  <p>Zero-config REST API mock server</p>
608
625
  <div class="banner-meta">
609
626
  <span>URL <strong>${host}</strong></span>
@@ -655,7 +672,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
655
672
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
656
673
 
657
674
  <footer>
658
- Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
675
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
659
676
  </footer>
660
677
 
661
678
  </div>
@@ -960,7 +977,63 @@ var CollectionRouteCommand = class {
960
977
  }
961
978
  };
962
979
 
980
+ // src/utils/conditions.ts
981
+ function resolveRequestPath(dotPath, req) {
982
+ const [root, ...rest] = dotPath.split(".");
983
+ let value;
984
+ switch (root) {
985
+ case "body":
986
+ value = req.body;
987
+ break;
988
+ case "params":
989
+ value = req.params;
990
+ break;
991
+ case "query":
992
+ value = req.query;
993
+ break;
994
+ case "headers":
995
+ value = req.headers;
996
+ break;
997
+ default:
998
+ return void 0;
999
+ }
1000
+ for (const key of rest) {
1001
+ if (value != null && typeof value === "object") {
1002
+ value = value[key];
1003
+ } else {
1004
+ return void 0;
1005
+ }
1006
+ }
1007
+ return value;
1008
+ }
1009
+ function matchConditionGroup(group, req) {
1010
+ return Object.entries(group).every(([key, expected]) => {
1011
+ const op = OPERATORS.find((o) => key.endsWith(o));
1012
+ if (op) {
1013
+ const path = key.slice(0, -op.length);
1014
+ const value2 = resolveRequestPath(path, req);
1015
+ if (value2 === void 0) return false;
1016
+ return applyOperator(value2, op, String(expected));
1017
+ }
1018
+ const value = resolveRequestPath(key, req);
1019
+ return String(value) === String(expected);
1020
+ });
1021
+ }
1022
+ function matchWhen(when, req) {
1023
+ if (Array.isArray(when)) {
1024
+ return when.some((group) => matchConditionGroup(group, req));
1025
+ }
1026
+ return matchConditionGroup(when, req);
1027
+ }
1028
+ function findMatchingScenario(scenarios, req) {
1029
+ return scenarios.find((s) => matchWhen(s.when, req));
1030
+ }
1031
+
963
1032
  // src/router/routes/custom.routes.ts
1033
+ function resolveBody(body, ctx) {
1034
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1035
+ return body ?? null;
1036
+ }
964
1037
  var CustomRouteCommand = class {
965
1038
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
966
1039
  this.storage = storage;
@@ -985,6 +1058,9 @@ var CustomRouteCommand = class {
985
1058
  method,
986
1059
  url,
987
1060
  handler: async (req, reply) => {
1061
+ if (route.delay && route.delay > 0) {
1062
+ await new Promise((resolve5) => setTimeout(resolve5, route.delay));
1063
+ }
988
1064
  for (const [key, value] of Object.entries(headers)) {
989
1065
  reply.header(key, value);
990
1066
  }
@@ -994,13 +1070,13 @@ var CustomRouteCommand = class {
994
1070
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
995
1071
  }
996
1072
  try {
997
- const ctx = {
1073
+ const ctx2 = {
998
1074
  params: req.params,
999
1075
  query: req.query,
1000
1076
  body: req.body,
1001
1077
  headers: req.headers
1002
1078
  };
1003
- const result = await fn(ctx);
1079
+ const result = await fn(ctx2);
1004
1080
  const resStatus = result.status ?? 200;
1005
1081
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1006
1082
  reply.header(k, v);
@@ -1012,12 +1088,24 @@ var CustomRouteCommand = class {
1012
1088
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1013
1089
  }
1014
1090
  }
1015
- const body = dynamic ? interpolate(rawBody, {
1091
+ const ctx = {
1016
1092
  params: req.params,
1017
1093
  query: req.query,
1018
1094
  body: req.body,
1019
1095
  headers: req.headers
1020
- }) : rawBody;
1096
+ };
1097
+ if (route.scenarios?.length) {
1098
+ const matched = findMatchingScenario(route.scenarios, ctx);
1099
+ const active = matched?.response ?? route.otherwise;
1100
+ if (active) {
1101
+ const aStatus = active.status ?? 200;
1102
+ const aBody = resolveBody(active.body, ctx);
1103
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1104
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1105
+ return reply.status(aStatus).send(aBody);
1106
+ }
1107
+ }
1108
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1021
1109
  if (body === null && status === 204) return reply.status(status).send();
1022
1110
  return reply.status(status).send(body);
1023
1111
  }
package/dist/index.d.mts CHANGED
@@ -21,19 +21,72 @@ type Data = Record<string, Resource[]>;
21
21
  * // GET /users/1/posts → returns posts where userId === "1"
22
22
  */
23
23
  type Relations = Record<string, Record<string, string>>;
24
+ /**
25
+ * A static response block shared by {@link CustomRoute} and {@link Scenario}.
26
+ */
27
+ type RouteResponse = {
28
+ /** HTTP status code. Defaults to `200` if omitted. */
29
+ status?: number;
30
+ /** Response body. Any YAML-serialisable value (object, array, string, number, null). */
31
+ body?: unknown;
32
+ /** Additional response headers to set alongside `Content-Type`. */
33
+ headers?: Record<string, string>;
34
+ };
35
+ /**
36
+ * A conditional response variant for a custom route.
37
+ *
38
+ * Evaluated in declaration order — the first scenario whose `when` conditions match wins.
39
+ * If none match, the route falls back to `otherwise:` (if defined) or `response:`.
40
+ *
41
+ * **`when` as an object** — all entries must match (AND):
42
+ * ```yaml
43
+ * when: { body.email: ana@test.com, body.password: secret }
44
+ * ```
45
+ *
46
+ * **`when` as an array** — any group must match (OR of ANDs):
47
+ * ```yaml
48
+ * when:
49
+ * - { body.role: admin }
50
+ * - { body.role: superadmin }
51
+ * ```
52
+ *
53
+ * Condition keys use dot-notation (`body.X`, `params.X`, `query.X`, `headers.X`).
54
+ * Field operator suffixes are supported: `_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`.
55
+ * Response bodies support `{{}}` template variables (same as static routes).
56
+ */
57
+ type Scenario = {
58
+ /**
59
+ * Condition(s) to evaluate against the request.
60
+ * - Object → all entries AND
61
+ * - Array of objects → any group OR (each group is AND internally)
62
+ */
63
+ when: Record<string, unknown> | Record<string, unknown>[];
64
+ /** Response to return when the conditions match. Supports `{{}}` template variables. */
65
+ response: RouteResponse;
66
+ };
24
67
  /**
25
68
  * A single custom route declared under `_routes` in the YAML file.
26
69
  *
27
- * Custom routes are registered before resource routes and take priority over them.
28
- * They always return a static, pre-defined response regardless of the request body or params.
70
+ * Resolution priority per request:
71
+ * 1. `handler` function (if defined and found in the handlers file)
72
+ * 2. First matching `scenario` (evaluated in declaration order)
73
+ * 3. `otherwise` block (explicit fallback when scenarios are defined but none matched)
74
+ * 4. Static `response` block (final fallback)
29
75
  *
30
76
  * @example
31
77
  * // _routes:
32
78
  * // - method: POST
33
79
  * // path: /login
34
- * // response:
35
- * // status: 200
36
- * // body: { token: abc123 }
80
+ * // scenarios:
81
+ * // - when: { body.password: secret }
82
+ * // response: { status: 200, body: { token: real-tok } }
83
+ * // - when:
84
+ * // - { body.role: admin }
85
+ * // - { body.role: superadmin }
86
+ * // response: { status: 200, body: { token: admin-tok } }
87
+ * // otherwise:
88
+ * // status: 401
89
+ * // body: { error: Invalid credentials }
37
90
  */
38
91
  type CustomRoute = {
39
92
  /** HTTP method (case-insensitive: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). */
@@ -42,19 +95,23 @@ type CustomRoute = {
42
95
  path: string;
43
96
  /**
44
97
  * Name of an exported function in the handlers file (`handlers:` in config).
45
- * When set, the function is called on every request and its return value is used as the response.
46
- * Takes priority over `response:`. Falls back to `response:` if the name is not found.
98
+ * Takes priority over `scenarios` and `response`. Falls back to `response` if not found.
47
99
  */
48
100
  handler?: string;
49
- /** Static or template response. Used when `handler` is absent or not found in the handlers file. */
50
- response?: {
51
- /** HTTP status code. Defaults to `200` if omitted. */
52
- status?: number;
53
- /** Response body. Any YAML-serialisable value (object, array, string, number, null). */
54
- body?: unknown;
55
- /** Additional response headers to set alongside `Content-Type`. */
56
- headers?: Record<string, string>;
57
- };
101
+ /** Conditional response variants. Evaluated in order first match wins. */
102
+ scenarios?: Scenario[];
103
+ /**
104
+ * Explicit fallback response when `scenarios` are defined but none matched.
105
+ * Takes priority over `response` when present. Supports `{{}}` template variables.
106
+ */
107
+ otherwise?: RouteResponse;
108
+ /** Static or template response. Final fallback when no handler, scenario, or otherwise applies. */
109
+ response?: RouteResponse;
110
+ /**
111
+ * Per-route response delay in milliseconds. Overrides the global `--delay` option for this route.
112
+ * Applied before any response is sent, regardless of which path resolved the response.
113
+ */
114
+ delay?: number;
58
115
  };
59
116
  /**
60
117
  * In-memory store backed by a YAML file.
package/dist/index.d.ts CHANGED
@@ -21,19 +21,72 @@ type Data = Record<string, Resource[]>;
21
21
  * // GET /users/1/posts → returns posts where userId === "1"
22
22
  */
23
23
  type Relations = Record<string, Record<string, string>>;
24
+ /**
25
+ * A static response block shared by {@link CustomRoute} and {@link Scenario}.
26
+ */
27
+ type RouteResponse = {
28
+ /** HTTP status code. Defaults to `200` if omitted. */
29
+ status?: number;
30
+ /** Response body. Any YAML-serialisable value (object, array, string, number, null). */
31
+ body?: unknown;
32
+ /** Additional response headers to set alongside `Content-Type`. */
33
+ headers?: Record<string, string>;
34
+ };
35
+ /**
36
+ * A conditional response variant for a custom route.
37
+ *
38
+ * Evaluated in declaration order — the first scenario whose `when` conditions match wins.
39
+ * If none match, the route falls back to `otherwise:` (if defined) or `response:`.
40
+ *
41
+ * **`when` as an object** — all entries must match (AND):
42
+ * ```yaml
43
+ * when: { body.email: ana@test.com, body.password: secret }
44
+ * ```
45
+ *
46
+ * **`when` as an array** — any group must match (OR of ANDs):
47
+ * ```yaml
48
+ * when:
49
+ * - { body.role: admin }
50
+ * - { body.role: superadmin }
51
+ * ```
52
+ *
53
+ * Condition keys use dot-notation (`body.X`, `params.X`, `query.X`, `headers.X`).
54
+ * Field operator suffixes are supported: `_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`.
55
+ * Response bodies support `{{}}` template variables (same as static routes).
56
+ */
57
+ type Scenario = {
58
+ /**
59
+ * Condition(s) to evaluate against the request.
60
+ * - Object → all entries AND
61
+ * - Array of objects → any group OR (each group is AND internally)
62
+ */
63
+ when: Record<string, unknown> | Record<string, unknown>[];
64
+ /** Response to return when the conditions match. Supports `{{}}` template variables. */
65
+ response: RouteResponse;
66
+ };
24
67
  /**
25
68
  * A single custom route declared under `_routes` in the YAML file.
26
69
  *
27
- * Custom routes are registered before resource routes and take priority over them.
28
- * They always return a static, pre-defined response regardless of the request body or params.
70
+ * Resolution priority per request:
71
+ * 1. `handler` function (if defined and found in the handlers file)
72
+ * 2. First matching `scenario` (evaluated in declaration order)
73
+ * 3. `otherwise` block (explicit fallback when scenarios are defined but none matched)
74
+ * 4. Static `response` block (final fallback)
29
75
  *
30
76
  * @example
31
77
  * // _routes:
32
78
  * // - method: POST
33
79
  * // path: /login
34
- * // response:
35
- * // status: 200
36
- * // body: { token: abc123 }
80
+ * // scenarios:
81
+ * // - when: { body.password: secret }
82
+ * // response: { status: 200, body: { token: real-tok } }
83
+ * // - when:
84
+ * // - { body.role: admin }
85
+ * // - { body.role: superadmin }
86
+ * // response: { status: 200, body: { token: admin-tok } }
87
+ * // otherwise:
88
+ * // status: 401
89
+ * // body: { error: Invalid credentials }
37
90
  */
38
91
  type CustomRoute = {
39
92
  /** HTTP method (case-insensitive: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). */
@@ -42,19 +95,23 @@ type CustomRoute = {
42
95
  path: string;
43
96
  /**
44
97
  * Name of an exported function in the handlers file (`handlers:` in config).
45
- * When set, the function is called on every request and its return value is used as the response.
46
- * Takes priority over `response:`. Falls back to `response:` if the name is not found.
98
+ * Takes priority over `scenarios` and `response`. Falls back to `response` if not found.
47
99
  */
48
100
  handler?: string;
49
- /** Static or template response. Used when `handler` is absent or not found in the handlers file. */
50
- response?: {
51
- /** HTTP status code. Defaults to `200` if omitted. */
52
- status?: number;
53
- /** Response body. Any YAML-serialisable value (object, array, string, number, null). */
54
- body?: unknown;
55
- /** Additional response headers to set alongside `Content-Type`. */
56
- headers?: Record<string, string>;
57
- };
101
+ /** Conditional response variants. Evaluated in order first match wins. */
102
+ scenarios?: Scenario[];
103
+ /**
104
+ * Explicit fallback response when `scenarios` are defined but none matched.
105
+ * Takes priority over `response` when present. Supports `{{}}` template variables.
106
+ */
107
+ otherwise?: RouteResponse;
108
+ /** Static or template response. Final fallback when no handler, scenario, or otherwise applies. */
109
+ response?: RouteResponse;
110
+ /**
111
+ * Per-route response delay in milliseconds. Overrides the global `--delay` option for this route.
112
+ * Applied before any response is sent, regardless of which path resolved the response.
113
+ */
114
+ delay?: number;
58
115
  };
59
116
  /**
60
117
  * In-memory store backed by a YAML file.
package/dist/index.js CHANGED
@@ -485,16 +485,33 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
485
485
  <table><tbody>
486
486
  ${customRoutes.map((r) => {
487
487
  const fullPath = `${base}${r.path}`;
488
+ const tags = [];
489
+ if (r.delay && r.delay > 0) {
490
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
491
+ }
492
+ if (r.scenarios?.length) {
493
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
494
+ tags.push(
495
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
496
+ );
497
+ }
498
+ if (r.otherwise) {
499
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
500
+ }
488
501
  let desc;
489
502
  if (r.handler) {
490
503
  const found = handlers.has(r.handler);
491
504
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
505
+ } else if (r.scenarios?.length) {
506
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
507
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
492
508
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
493
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
509
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
494
510
  } else {
495
511
  const status = r.response?.status ?? 200;
496
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
512
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
497
513
  }
514
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
498
515
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
499
516
  }).join("")}
500
517
  </tbody></table>
@@ -646,7 +663,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
646
663
 
647
664
  <div class="banner">
648
665
  <div class="banner-inner">
649
- <h1><span class="y">y</span><span class="rest">rest</span></h1>
666
+ <h1><span class="y">y</span><span class="rest">Rest</span></h1>
650
667
  <p>Zero-config REST API mock server</p>
651
668
  <div class="banner-meta">
652
669
  <span>URL <strong>${host}</strong></span>
@@ -698,7 +715,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
698
715
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
699
716
 
700
717
  <footer>
701
- Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
718
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
702
719
  </footer>
703
720
 
704
721
  </div>
@@ -1012,6 +1029,65 @@ var CollectionRouteCommand = class {
1012
1029
 
1013
1030
  // src/router/routes/custom.routes.ts
1014
1031
  init_cjs_shims();
1032
+
1033
+ // src/utils/conditions.ts
1034
+ init_cjs_shims();
1035
+ function resolveRequestPath(dotPath, req) {
1036
+ const [root, ...rest] = dotPath.split(".");
1037
+ let value;
1038
+ switch (root) {
1039
+ case "body":
1040
+ value = req.body;
1041
+ break;
1042
+ case "params":
1043
+ value = req.params;
1044
+ break;
1045
+ case "query":
1046
+ value = req.query;
1047
+ break;
1048
+ case "headers":
1049
+ value = req.headers;
1050
+ break;
1051
+ default:
1052
+ return void 0;
1053
+ }
1054
+ for (const key of rest) {
1055
+ if (value != null && typeof value === "object") {
1056
+ value = value[key];
1057
+ } else {
1058
+ return void 0;
1059
+ }
1060
+ }
1061
+ return value;
1062
+ }
1063
+ function matchConditionGroup(group, req) {
1064
+ return Object.entries(group).every(([key, expected]) => {
1065
+ const op = OPERATORS.find((o) => key.endsWith(o));
1066
+ if (op) {
1067
+ const path = key.slice(0, -op.length);
1068
+ const value2 = resolveRequestPath(path, req);
1069
+ if (value2 === void 0) return false;
1070
+ return applyOperator(value2, op, String(expected));
1071
+ }
1072
+ const value = resolveRequestPath(key, req);
1073
+ return String(value) === String(expected);
1074
+ });
1075
+ }
1076
+ function matchWhen(when, req) {
1077
+ if (Array.isArray(when)) {
1078
+ return when.some((group) => matchConditionGroup(group, req));
1079
+ }
1080
+ return matchConditionGroup(when, req);
1081
+ }
1082
+ function findMatchingScenario(scenarios, req) {
1083
+ return scenarios.find((s) => matchWhen(s.when, req));
1084
+ }
1085
+
1086
+ // src/router/routes/custom.routes.ts
1087
+ function resolveBody(body, ctx) {
1088
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1089
+ return body ?? null;
1090
+ }
1015
1091
  var CustomRouteCommand = class {
1016
1092
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
1017
1093
  this.storage = storage;
@@ -1036,6 +1112,9 @@ var CustomRouteCommand = class {
1036
1112
  method,
1037
1113
  url,
1038
1114
  handler: async (req, reply) => {
1115
+ if (route.delay && route.delay > 0) {
1116
+ await new Promise((resolve3) => setTimeout(resolve3, route.delay));
1117
+ }
1039
1118
  for (const [key, value] of Object.entries(headers)) {
1040
1119
  reply.header(key, value);
1041
1120
  }
@@ -1045,13 +1124,13 @@ var CustomRouteCommand = class {
1045
1124
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1046
1125
  }
1047
1126
  try {
1048
- const ctx = {
1127
+ const ctx2 = {
1049
1128
  params: req.params,
1050
1129
  query: req.query,
1051
1130
  body: req.body,
1052
1131
  headers: req.headers
1053
1132
  };
1054
- const result = await fn(ctx);
1133
+ const result = await fn(ctx2);
1055
1134
  const resStatus = result.status ?? 200;
1056
1135
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1057
1136
  reply.header(k, v);
@@ -1063,12 +1142,24 @@ var CustomRouteCommand = class {
1063
1142
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1064
1143
  }
1065
1144
  }
1066
- const body = dynamic ? interpolate(rawBody, {
1145
+ const ctx = {
1067
1146
  params: req.params,
1068
1147
  query: req.query,
1069
1148
  body: req.body,
1070
1149
  headers: req.headers
1071
- }) : rawBody;
1150
+ };
1151
+ if (route.scenarios?.length) {
1152
+ const matched = findMatchingScenario(route.scenarios, ctx);
1153
+ const active = matched?.response ?? route.otherwise;
1154
+ if (active) {
1155
+ const aStatus = active.status ?? 200;
1156
+ const aBody = resolveBody(active.body, ctx);
1157
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1158
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1159
+ return reply.status(aStatus).send(aBody);
1160
+ }
1161
+ }
1162
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1072
1163
  if (body === null && status === 204) return reply.status(status).send();
1073
1164
  return reply.status(status).send(body);
1074
1165
  }
package/dist/index.mjs CHANGED
@@ -454,16 +454,33 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
454
454
  <table><tbody>
455
455
  ${customRoutes.map((r) => {
456
456
  const fullPath = `${base}${r.path}`;
457
+ const tags = [];
458
+ if (r.delay && r.delay > 0) {
459
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
460
+ }
461
+ if (r.scenarios?.length) {
462
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
463
+ tags.push(
464
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
465
+ );
466
+ }
467
+ if (r.otherwise) {
468
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
469
+ }
457
470
  let desc;
458
471
  if (r.handler) {
459
472
  const found = handlers.has(r.handler);
460
473
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
474
+ } else if (r.scenarios?.length) {
475
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
476
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
461
477
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
462
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
478
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
463
479
  } else {
464
480
  const status = r.response?.status ?? 200;
465
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
481
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
466
482
  }
483
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
467
484
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
468
485
  }).join("")}
469
486
  </tbody></table>
@@ -615,7 +632,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
615
632
 
616
633
  <div class="banner">
617
634
  <div class="banner-inner">
618
- <h1><span class="y">y</span><span class="rest">rest</span></h1>
635
+ <h1><span class="y">y</span><span class="rest">Rest</span></h1>
619
636
  <p>Zero-config REST API mock server</p>
620
637
  <div class="banner-meta">
621
638
  <span>URL <strong>${host}</strong></span>
@@ -667,7 +684,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
667
684
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
668
685
 
669
686
  <footer>
670
- Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
687
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
671
688
  </footer>
672
689
 
673
690
  </div>
@@ -981,6 +998,65 @@ var CollectionRouteCommand = class {
981
998
 
982
999
  // src/router/routes/custom.routes.ts
983
1000
  init_esm_shims();
1001
+
1002
+ // src/utils/conditions.ts
1003
+ init_esm_shims();
1004
+ function resolveRequestPath(dotPath, req) {
1005
+ const [root, ...rest] = dotPath.split(".");
1006
+ let value;
1007
+ switch (root) {
1008
+ case "body":
1009
+ value = req.body;
1010
+ break;
1011
+ case "params":
1012
+ value = req.params;
1013
+ break;
1014
+ case "query":
1015
+ value = req.query;
1016
+ break;
1017
+ case "headers":
1018
+ value = req.headers;
1019
+ break;
1020
+ default:
1021
+ return void 0;
1022
+ }
1023
+ for (const key of rest) {
1024
+ if (value != null && typeof value === "object") {
1025
+ value = value[key];
1026
+ } else {
1027
+ return void 0;
1028
+ }
1029
+ }
1030
+ return value;
1031
+ }
1032
+ function matchConditionGroup(group, req) {
1033
+ return Object.entries(group).every(([key, expected]) => {
1034
+ const op = OPERATORS.find((o) => key.endsWith(o));
1035
+ if (op) {
1036
+ const path2 = key.slice(0, -op.length);
1037
+ const value2 = resolveRequestPath(path2, req);
1038
+ if (value2 === void 0) return false;
1039
+ return applyOperator(value2, op, String(expected));
1040
+ }
1041
+ const value = resolveRequestPath(key, req);
1042
+ return String(value) === String(expected);
1043
+ });
1044
+ }
1045
+ function matchWhen(when, req) {
1046
+ if (Array.isArray(when)) {
1047
+ return when.some((group) => matchConditionGroup(group, req));
1048
+ }
1049
+ return matchConditionGroup(when, req);
1050
+ }
1051
+ function findMatchingScenario(scenarios, req) {
1052
+ return scenarios.find((s) => matchWhen(s.when, req));
1053
+ }
1054
+
1055
+ // src/router/routes/custom.routes.ts
1056
+ function resolveBody(body, ctx) {
1057
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1058
+ return body ?? null;
1059
+ }
984
1060
  var CustomRouteCommand = class {
985
1061
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
986
1062
  this.storage = storage;
@@ -1005,6 +1081,9 @@ var CustomRouteCommand = class {
1005
1081
  method,
1006
1082
  url,
1007
1083
  handler: async (req, reply) => {
1084
+ if (route.delay && route.delay > 0) {
1085
+ await new Promise((resolve3) => setTimeout(resolve3, route.delay));
1086
+ }
1008
1087
  for (const [key, value] of Object.entries(headers)) {
1009
1088
  reply.header(key, value);
1010
1089
  }
@@ -1014,13 +1093,13 @@ var CustomRouteCommand = class {
1014
1093
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1015
1094
  }
1016
1095
  try {
1017
- const ctx = {
1096
+ const ctx2 = {
1018
1097
  params: req.params,
1019
1098
  query: req.query,
1020
1099
  body: req.body,
1021
1100
  headers: req.headers
1022
1101
  };
1023
- const result = await fn(ctx);
1102
+ const result = await fn(ctx2);
1024
1103
  const resStatus = result.status ?? 200;
1025
1104
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1026
1105
  reply.header(k, v);
@@ -1032,12 +1111,24 @@ var CustomRouteCommand = class {
1032
1111
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1033
1112
  }
1034
1113
  }
1035
- const body = dynamic ? interpolate(rawBody, {
1114
+ const ctx = {
1036
1115
  params: req.params,
1037
1116
  query: req.query,
1038
1117
  body: req.body,
1039
1118
  headers: req.headers
1040
- }) : rawBody;
1119
+ };
1120
+ if (route.scenarios?.length) {
1121
+ const matched = findMatchingScenario(route.scenarios, ctx);
1122
+ const active = matched?.response ?? route.otherwise;
1123
+ if (active) {
1124
+ const aStatus = active.status ?? 200;
1125
+ const aBody = resolveBody(active.body, ctx);
1126
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1127
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1128
+ return reply.status(aStatus).send(aBody);
1129
+ }
1130
+ }
1131
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1041
1132
  if (body === null && status === 204) return reply.status(status).send();
1042
1133
  return reply.status(status).send(body);
1043
1134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.",
5
5
  "keywords": [
6
6
  "yrest",