@yrest/cli 0.5.3 → 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/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/cli/index.js +133 -17
- package/dist/cli/index.mjs +133 -17
- package/dist/index.d.mts +205 -30
- package/dist/index.d.ts +205 -30
- package/dist/index.js +363 -33
- package/dist/index.mjs +378 -41
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aggiovato
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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`) | ✅ | ❌ |
|
|
@@ -63,6 +64,7 @@ A YAML-first alternative to json-server for frontend development.
|
|
|
63
64
|
| Readonly mode | ✅ | ❌ |
|
|
64
65
|
| Atomic writes | ✅ | ✅ |
|
|
65
66
|
| TypeScript types | ✅ | ❌ |
|
|
67
|
+
| Programmatic API for test frameworks | ✅ | ❌ |
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
|
@@ -496,6 +498,103 @@ Available variables:
|
|
|
496
498
|
|
|
497
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.
|
|
498
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
|
+
|
|
499
598
|
### Handler functions
|
|
500
599
|
|
|
501
600
|
For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the `handler:` field:
|
|
@@ -694,6 +793,139 @@ const session = await fetch("http://localhost:3070/login", {
|
|
|
694
793
|
// → { token: "tok-ana@test.com" }
|
|
695
794
|
```
|
|
696
795
|
|
|
796
|
+
## Programmatic API
|
|
797
|
+
|
|
798
|
+
Use yRest directly inside your test suite — no CLI, no separate process to manage.
|
|
799
|
+
|
|
800
|
+
Install as a dev dependency:
|
|
801
|
+
|
|
802
|
+
```bash
|
|
803
|
+
npm install -D @yrest/cli
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### `createYrestServer`
|
|
807
|
+
|
|
808
|
+
Creates a server instance that you control with `start()` and `stop()`. Accepts either inline YAML data or a path to a `db.yml` file.
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
import { createYrestServer, yrest } from "@yrest/cli";
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
**Options:**
|
|
815
|
+
|
|
816
|
+
| Option | Type | Default | Description |
|
|
817
|
+
| ---------- | ----------------- | ----------- | ------------------------------------------------------ |
|
|
818
|
+
| `data` | `Data` | — | Inline data object (use with `yrest\`...\``) |
|
|
819
|
+
| `file` | `string` | — | Path to a `db.yml` file (`data` or `file` is required) |
|
|
820
|
+
| `port` | `number` | `3070` | TCP port. Use `0` to get a random available port |
|
|
821
|
+
| `host` | `string` | `localhost` | Host to bind |
|
|
822
|
+
| `base` | `string` | — | URL prefix for all routes (e.g. `"/api"`) |
|
|
823
|
+
| `readonly` | `boolean` | `false` | Reject all write operations with `405` |
|
|
824
|
+
| `delay` | `number` | `0` | Fixed delay in ms added to every response |
|
|
825
|
+
| `pageable` | `boolean\|number` | `false` | Wrap GET responses in `{ data, pagination }` envelope |
|
|
826
|
+
| `snapshot` | `boolean` | `false` | Enable snapshot endpoints (`/_snapshot`) |
|
|
827
|
+
| `handlers` | `string` | — | Path to a JS file exporting handler functions |
|
|
828
|
+
|
|
829
|
+
**Returned handle:**
|
|
830
|
+
|
|
831
|
+
| Member | Description |
|
|
832
|
+
| --------- | -------------------------------------------------------- |
|
|
833
|
+
| `start()` | Starts the server and begins listening |
|
|
834
|
+
| `stop()` | Gracefully closes the server |
|
|
835
|
+
| `port` | The actual port after `start()` (useful when `port: 0`) |
|
|
836
|
+
| `url` | Base URL after `start()` (e.g. `http://localhost:49821`) |
|
|
837
|
+
|
|
838
|
+
### `yrest` tagged template literal
|
|
839
|
+
|
|
840
|
+
Parses inline YAML into a data object. Strips common leading indentation automatically, so you can indent naturally inside your test files. Supports interpolated values.
|
|
841
|
+
|
|
842
|
+
```ts
|
|
843
|
+
const data = yrest`
|
|
844
|
+
users:
|
|
845
|
+
- id: 1
|
|
846
|
+
name: Ana
|
|
847
|
+
posts:
|
|
848
|
+
- id: 1
|
|
849
|
+
title: First post
|
|
850
|
+
userId: 1
|
|
851
|
+
`;
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Vitest example
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
858
|
+
import { createYrestServer, yrest } from "@yrest/cli";
|
|
859
|
+
|
|
860
|
+
const server = createYrestServer({
|
|
861
|
+
data: yrest`
|
|
862
|
+
users:
|
|
863
|
+
- id: 1
|
|
864
|
+
name: Ana
|
|
865
|
+
- id: 2
|
|
866
|
+
name: Luis
|
|
867
|
+
`,
|
|
868
|
+
port: 0, // random port — no conflicts between parallel tests
|
|
869
|
+
readonly: true,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
beforeAll(() => server.start());
|
|
873
|
+
afterAll(() => server.stop());
|
|
874
|
+
|
|
875
|
+
describe("users API", () => {
|
|
876
|
+
it("returns all users", async () => {
|
|
877
|
+
const res = await fetch(`${server.url}/users`);
|
|
878
|
+
expect(res.status).toBe(200);
|
|
879
|
+
expect(await res.json()).toHaveLength(2);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it("returns a single user", async () => {
|
|
883
|
+
const res = await fetch(`${server.url}/users/1`);
|
|
884
|
+
expect(await res.json()).toMatchObject({ name: "Ana" });
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Playwright example
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
// tests/api.spec.ts
|
|
893
|
+
import { test, expect, beforeAll, afterAll } from "@playwright/test";
|
|
894
|
+
import { createYrestServer, yrest } from "@yrest/cli";
|
|
895
|
+
|
|
896
|
+
const server = createYrestServer({
|
|
897
|
+
data: yrest`
|
|
898
|
+
users:
|
|
899
|
+
- id: 1
|
|
900
|
+
name: Ana
|
|
901
|
+
`,
|
|
902
|
+
port: 0,
|
|
903
|
+
readonly: true,
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
beforeAll(() => server.start());
|
|
907
|
+
afterAll(() => server.stop());
|
|
908
|
+
|
|
909
|
+
test("lists users", async () => {
|
|
910
|
+
const res = await fetch(`${server.url}/users`);
|
|
911
|
+
expect(await res.json()).toHaveLength(1);
|
|
912
|
+
});
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### File-based example
|
|
916
|
+
|
|
917
|
+
When your test data is too large for inline YAML, point to a file:
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
const server = createYrestServer({
|
|
921
|
+
file: "./tests/fixtures/db.yml",
|
|
922
|
+
port: 0,
|
|
923
|
+
readonly: true,
|
|
924
|
+
});
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
697
929
|
## Use in package.json scripts
|
|
698
930
|
|
|
699
931
|
```json
|
|
@@ -708,6 +940,29 @@ const session = await fetch("http://localhost:3070/login", {
|
|
|
708
940
|
|
|
709
941
|
---
|
|
710
942
|
|
|
943
|
+
## Roadmap
|
|
944
|
+
|
|
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:`) | ✅ |
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
711
966
|
## Contributing
|
|
712
967
|
|
|
713
968
|
### Prerequisites
|
package/dist/cli/index.js
CHANGED
|
@@ -135,7 +135,7 @@ function registerInit(program2) {
|
|
|
135
135
|
var import_node_fs5 = require("fs");
|
|
136
136
|
var import_node_path3 = require("path");
|
|
137
137
|
|
|
138
|
-
// src/storage/
|
|
138
|
+
// src/storage/yrestStorage.ts
|
|
139
139
|
var import_node_fs2 = require("fs");
|
|
140
140
|
var import_node_path2 = require("path");
|
|
141
141
|
var import_node_crypto = require("crypto");
|
|
@@ -148,8 +148,8 @@ function deepCopyData(source) {
|
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
// src/storage/
|
|
152
|
-
function
|
|
151
|
+
// src/storage/yrestStorage.ts
|
|
152
|
+
function createYrestStorage(filePath) {
|
|
153
153
|
const absPath = (0, import_node_path2.resolve)(filePath);
|
|
154
154
|
const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
|
|
155
155
|
const relations = raw["_rel"] ?? {};
|
|
@@ -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
|
}
|
|
@@ -1231,9 +1319,37 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
|
|
|
1231
1319
|
return server;
|
|
1232
1320
|
}
|
|
1233
1321
|
|
|
1322
|
+
// src/server/yrestServer.ts
|
|
1323
|
+
function createYrestServerFromStorage(storage, options, handlers = /* @__PURE__ */ new Map()) {
|
|
1324
|
+
let _port = 0;
|
|
1325
|
+
let _started = false;
|
|
1326
|
+
let _fastify = null;
|
|
1327
|
+
return {
|
|
1328
|
+
async start() {
|
|
1329
|
+
if (_started) return;
|
|
1330
|
+
_fastify = await createServer(storage, options, handlers);
|
|
1331
|
+
await _fastify.listen({ port: options.port, host: options.host });
|
|
1332
|
+
const address = _fastify.addresses()[0];
|
|
1333
|
+
_port = typeof address === "object" && "port" in address ? address.port : options.port;
|
|
1334
|
+
_started = true;
|
|
1335
|
+
},
|
|
1336
|
+
async stop() {
|
|
1337
|
+
if (!_started || !_fastify) return;
|
|
1338
|
+
await _fastify.close();
|
|
1339
|
+
_started = false;
|
|
1340
|
+
},
|
|
1341
|
+
get port() {
|
|
1342
|
+
return _port;
|
|
1343
|
+
},
|
|
1344
|
+
get url() {
|
|
1345
|
+
return `http://${options.host}:${_port}${options.base}`;
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1234
1350
|
// src/config/loadOptions.ts
|
|
1235
1351
|
var import_zod = require("zod");
|
|
1236
|
-
var
|
|
1352
|
+
var yrestOptionsSchema = import_zod.z.object({
|
|
1237
1353
|
/** Path to the YAML database file. Must be a non-empty string. */
|
|
1238
1354
|
file: import_zod.z.string().min(1),
|
|
1239
1355
|
/** TCP port the server listens on. Accepts string input and coerces to number. */
|
|
@@ -1324,18 +1440,18 @@ function registerServe(program2) {
|
|
|
1324
1440
|
...cmd.args.length > 0 ? { file } : {},
|
|
1325
1441
|
...cliOverrides
|
|
1326
1442
|
};
|
|
1327
|
-
const options =
|
|
1443
|
+
const options = yrestOptionsSchema.parse(merged);
|
|
1328
1444
|
let storage;
|
|
1329
1445
|
try {
|
|
1330
|
-
storage =
|
|
1446
|
+
storage = createYrestStorage(options.file);
|
|
1331
1447
|
} catch (err) {
|
|
1332
1448
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1333
1449
|
console.error(`Error: cannot load "${options.file}" \u2014 ${msg}`);
|
|
1334
1450
|
process.exit(1);
|
|
1335
1451
|
}
|
|
1336
1452
|
const handlers = options.handlers ? await loadHandlers((0, import_node_path3.resolve)(options.handlers)) : /* @__PURE__ */ new Map();
|
|
1337
|
-
const
|
|
1338
|
-
await
|
|
1453
|
+
const yrestServer = createYrestServerFromStorage(storage, options, handlers);
|
|
1454
|
+
await yrestServer.start();
|
|
1339
1455
|
const collections = Object.keys(storage.getData());
|
|
1340
1456
|
const customRoutes = storage.getRoutes();
|
|
1341
1457
|
const base = options.base || "/";
|
|
@@ -1355,7 +1471,7 @@ function registerServe(program2) {
|
|
|
1355
1471
|
};
|
|
1356
1472
|
console.log(
|
|
1357
1473
|
`
|
|
1358
|
-
${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${
|
|
1474
|
+
${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${yrestServer.port}`)}
|
|
1359
1475
|
`
|
|
1360
1476
|
);
|
|
1361
1477
|
console.log(` ${b("Collections")} ${dim(`(base: ${base})`)}:`);
|