@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 +116 -17
- package/dist/cli/index.js +96 -8
- package/dist/cli/index.mjs +96 -8
- package/dist/index.d.mts +73 -16
- package/dist/index.d.ts +73 -16
- package/dist/index.js +99 -8
- package/dist/index.mjs +99 -8
- package/package.json +1 -1
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
|
|
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
|
|
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 ? ` +
|
|
496
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
481
497
|
}
|
|
498
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
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">
|
|
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/
|
|
702
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <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
|
|
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(
|
|
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
|
|
1118
|
+
const ctx = {
|
|
1043
1119
|
params: req.params,
|
|
1044
1120
|
query: req.query,
|
|
1045
1121
|
body: req.body,
|
|
1046
1122
|
headers: req.headers
|
|
1047
|
-
}
|
|
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
|
}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
|
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 ? ` +
|
|
469
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
454
470
|
}
|
|
471
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
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">
|
|
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/
|
|
675
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <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
|
|
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(
|
|
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
|
|
1091
|
+
const ctx = {
|
|
1016
1092
|
params: req.params,
|
|
1017
1093
|
query: req.query,
|
|
1018
1094
|
body: req.body,
|
|
1019
1095
|
headers: req.headers
|
|
1020
|
-
}
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
* //
|
|
35
|
-
* //
|
|
36
|
-
* //
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
* //
|
|
35
|
-
* //
|
|
36
|
-
* //
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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 ? ` +
|
|
512
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
497
513
|
}
|
|
514
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
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">
|
|
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/
|
|
718
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <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
|
|
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(
|
|
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
|
|
1145
|
+
const ctx = {
|
|
1067
1146
|
params: req.params,
|
|
1068
1147
|
query: req.query,
|
|
1069
1148
|
body: req.body,
|
|
1070
1149
|
headers: req.headers
|
|
1071
|
-
}
|
|
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
|
|
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 ? ` +
|
|
481
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
466
482
|
}
|
|
483
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
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">
|
|
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/
|
|
687
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <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
|
|
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(
|
|
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
|
|
1114
|
+
const ctx = {
|
|
1036
1115
|
params: req.params,
|
|
1037
1116
|
query: req.query,
|
|
1038
1117
|
body: req.body,
|
|
1039
1118
|
headers: req.headers
|
|
1040
|
-
}
|
|
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