@yrest/cli 0.7.0 → 0.8.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 +40 -1
- package/assets/logo-color.png +0 -0
- package/assets/logo-figure.png +0 -0
- package/assets/logo-text.png +0 -0
- package/assets/logo-white.png +0 -0
- package/assets/yRest-banner.png +0 -0
- package/dist/cli/index.js +81 -36
- package/dist/cli/index.mjs +60 -15
- package/dist/index.d.mts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +61 -18
- package/dist/index.mjs +52 -12
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/yRest-banner.png" alt="yRest" width="100%" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<br/>
|
|
2
6
|
|
|
3
7
|
[](https://www.npmjs.com/package/@yrest/cli)
|
|
4
8
|
[](https://www.npmjs.com/package/@yrest/cli)
|
|
@@ -6,6 +10,7 @@
|
|
|
6
10
|
[](https://github.com/aggiovato/yRest/actions)
|
|
7
11
|
[](https://www.npmjs.com/package/@yrest/cli)
|
|
8
12
|
[](https://www.typescriptlang.org/)
|
|
13
|
+
[](https://socket.dev/npm/package/@yrest/cli)
|
|
9
14
|
|
|
10
15
|
YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
|
|
11
16
|
|
|
@@ -157,6 +162,7 @@ npx @yrest/cli serve db.yml --handlers yrest.handlers.js
|
|
|
157
162
|
| `--pageable [n]` | `false` | Wrap GET collection responses in `{ data, pagination }`. Optional limit |
|
|
158
163
|
| `--snapshot` | `false` | Save initial state snapshot and expose `/_snapshot` endpoints |
|
|
159
164
|
| `--handlers <file>` | _(none)_ | Path to a JS file exporting handler functions for custom routes |
|
|
165
|
+
| `--id-strategy` | `increment` | Id generation for new items: `increment` (auto-int) or `uuid` |
|
|
160
166
|
|
|
161
167
|
All flags can also be set in `yrest.config.yml` (see below). CLI flags always take priority over the config file.
|
|
162
168
|
|
|
@@ -178,6 +184,7 @@ host: localhost
|
|
|
178
184
|
# pageable: false # true (limit 10), or a number (custom limit)
|
|
179
185
|
# snapshot: false
|
|
180
186
|
# handlers: yrest.handlers.js
|
|
187
|
+
# idStrategy: increment # increment or uuid
|
|
181
188
|
```
|
|
182
189
|
|
|
183
190
|
**Priority order** (highest wins): CLI flags → `yrest.config.yml` → schema defaults.
|
|
@@ -595,6 +602,36 @@ _routes:
|
|
|
595
602
|
body: { data: loaded }
|
|
596
603
|
```
|
|
597
604
|
|
|
605
|
+
### Error injection
|
|
606
|
+
|
|
607
|
+
Force a custom route to always return a specific HTTP error, regardless of handlers, scenarios or the static response. Useful for simulating outages, payment failures or auth errors:
|
|
608
|
+
|
|
609
|
+
```yaml
|
|
610
|
+
_routes:
|
|
611
|
+
- method: GET
|
|
612
|
+
path: /payments
|
|
613
|
+
error: 503
|
|
614
|
+
errorBody:
|
|
615
|
+
message: Service temporarily unavailable
|
|
616
|
+
retryAfter: 30
|
|
617
|
+
|
|
618
|
+
- method: POST
|
|
619
|
+
path: /checkout
|
|
620
|
+
error: 402
|
|
621
|
+
errorBody:
|
|
622
|
+
error: Payment required
|
|
623
|
+
|
|
624
|
+
- method: GET
|
|
625
|
+
path: /slow-failure
|
|
626
|
+
delay: 400
|
|
627
|
+
error: 500
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
- `error` takes priority over `handler`, `scenarios` and `response` — the route always returns this status.
|
|
631
|
+
- `errorBody` is optional. If omitted, the default body is `{ "error": "Forced error NNN" }`.
|
|
632
|
+
- `delay` still applies before the error is returned.
|
|
633
|
+
- Shown in `/_about` with a red `error·NNN` badge.
|
|
634
|
+
|
|
598
635
|
### Handler functions
|
|
599
636
|
|
|
600
637
|
For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the `handler:` field:
|
|
@@ -960,6 +997,8 @@ const server = createYrestServer({
|
|
|
960
997
|
| Request validation with JSON Schema | 🔜 |
|
|
961
998
|
| Conditional scenarios (`scenarios:`, `otherwise:`) | ✅ |
|
|
962
999
|
| Per-route delay (`delay:`) | ✅ |
|
|
1000
|
+
| Error injection (`error:` in `_routes`) | ✅ |
|
|
1001
|
+
| Configurable ID strategy (`idStrategy`) | ✅ |
|
|
963
1002
|
|
|
964
1003
|
---
|
|
965
1004
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/index.js
CHANGED
|
@@ -132,8 +132,8 @@ function registerInit(program2) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// src/cli/commands/serve.ts
|
|
135
|
-
var
|
|
136
|
-
var
|
|
135
|
+
var import_node_fs6 = require("fs");
|
|
136
|
+
var import_node_path4 = require("path");
|
|
137
137
|
|
|
138
138
|
// src/storage/yrestStorage.ts
|
|
139
139
|
var import_node_fs2 = require("fs");
|
|
@@ -223,6 +223,11 @@ function createYrestStorage(filePath) {
|
|
|
223
223
|
var import_fastify = __toESM(require("fastify"));
|
|
224
224
|
var import_cors = __toESM(require("@fastify/cors"));
|
|
225
225
|
|
|
226
|
+
// src/router/templates/about.template.ts
|
|
227
|
+
var import_node_fs3 = require("fs");
|
|
228
|
+
var import_node_path3 = require("path");
|
|
229
|
+
var import_node_url = require("url");
|
|
230
|
+
|
|
226
231
|
// src/utils/interpolate.ts
|
|
227
232
|
var import_node_crypto2 = require("crypto");
|
|
228
233
|
function getPath(obj, path) {
|
|
@@ -270,6 +275,15 @@ function hasTemplates(value) {
|
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
// src/router/templates/about.template.ts
|
|
278
|
+
var _dir = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
279
|
+
var LOGO_SRC = (() => {
|
|
280
|
+
try {
|
|
281
|
+
const buf = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(_dir, "../../assets/logo-color.png"));
|
|
282
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
283
|
+
} catch {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
})();
|
|
273
287
|
var METHOD_COLOR = {
|
|
274
288
|
GET: "#3fb950",
|
|
275
289
|
POST: "#58a6ff",
|
|
@@ -428,6 +442,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
428
442
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
429
443
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
430
444
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
445
|
+
if (options.idStrategy !== "increment")
|
|
446
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
431
447
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
432
448
|
const nestedRows = [];
|
|
433
449
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -470,6 +486,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
470
486
|
${customRoutes.map((r) => {
|
|
471
487
|
const fullPath = `${base}${r.path}`;
|
|
472
488
|
const tags = [];
|
|
489
|
+
if (r.error) {
|
|
490
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
491
|
+
}
|
|
473
492
|
if (r.delay && r.delay > 0) {
|
|
474
493
|
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
475
494
|
}
|
|
@@ -483,7 +502,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
483
502
|
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
484
503
|
}
|
|
485
504
|
let desc;
|
|
486
|
-
if (r.
|
|
505
|
+
if (r.error) {
|
|
506
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
507
|
+
} else if (r.handler) {
|
|
487
508
|
const found = handlers.has(r.handler);
|
|
488
509
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
489
510
|
} else if (r.scenarios?.length) {
|
|
@@ -647,7 +668,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
647
668
|
|
|
648
669
|
<div class="banner">
|
|
649
670
|
<div class="banner-inner">
|
|
650
|
-
|
|
671
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
651
672
|
<p>Zero-config REST API mock server</p>
|
|
652
673
|
<div class="banner-meta">
|
|
653
674
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -730,6 +751,10 @@ function nextId(items) {
|
|
|
730
751
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
731
752
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
732
753
|
}
|
|
754
|
+
function generateId(items, strategy) {
|
|
755
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
756
|
+
return nextId(items);
|
|
757
|
+
}
|
|
733
758
|
function firstParam(value) {
|
|
734
759
|
if (value === void 0) return void 0;
|
|
735
760
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -817,10 +842,10 @@ function findById(items, id) {
|
|
|
817
842
|
function findIndexById(items, id) {
|
|
818
843
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
819
844
|
}
|
|
820
|
-
function createItem(storage, resource, body) {
|
|
845
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
821
846
|
const collection = storage.getCollection(resource) ?? [];
|
|
822
847
|
const item = {
|
|
823
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
848
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
824
849
|
...body
|
|
825
850
|
};
|
|
826
851
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -998,7 +1023,12 @@ var CollectionRouteCommand = class {
|
|
|
998
1023
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
999
1024
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
1000
1025
|
}
|
|
1001
|
-
const item = createItem(
|
|
1026
|
+
const item = createItem(
|
|
1027
|
+
this.storage,
|
|
1028
|
+
this.resource,
|
|
1029
|
+
req.body,
|
|
1030
|
+
this.options.idStrategy
|
|
1031
|
+
);
|
|
1002
1032
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
1003
1033
|
});
|
|
1004
1034
|
}
|
|
@@ -1088,6 +1118,10 @@ var CustomRouteCommand = class {
|
|
|
1088
1118
|
if (route.delay && route.delay > 0) {
|
|
1089
1119
|
await new Promise((resolve5) => setTimeout(resolve5, route.delay));
|
|
1090
1120
|
}
|
|
1121
|
+
if (route.error) {
|
|
1122
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1123
|
+
return reply.status(route.error).send(body2);
|
|
1124
|
+
}
|
|
1091
1125
|
for (const [key, value] of Object.entries(headers)) {
|
|
1092
1126
|
reply.header(key, value);
|
|
1093
1127
|
}
|
|
@@ -1384,22 +1418,29 @@ var yrestOptionsSchema = import_zod.z.object({
|
|
|
1384
1418
|
pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1385
1419
|
enabled: v !== false,
|
|
1386
1420
|
limit: v === false || v === true ? 10 : v
|
|
1387
|
-
}))
|
|
1421
|
+
})),
|
|
1422
|
+
/**
|
|
1423
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1424
|
+
*
|
|
1425
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1426
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1427
|
+
*/
|
|
1428
|
+
idStrategy: import_zod.z.enum(["increment", "uuid"]).default("increment")
|
|
1388
1429
|
});
|
|
1389
1430
|
|
|
1390
1431
|
// src/config/loadConfigFile.ts
|
|
1391
|
-
var
|
|
1432
|
+
var import_node_fs4 = require("fs");
|
|
1392
1433
|
var import_yaml2 = require("yaml");
|
|
1393
1434
|
function loadConfigFile(configPath) {
|
|
1394
|
-
if (!(0,
|
|
1395
|
-
const raw = (0,
|
|
1435
|
+
if (!(0, import_node_fs4.existsSync)(configPath)) return {};
|
|
1436
|
+
const raw = (0, import_node_fs4.readFileSync)(configPath, "utf8");
|
|
1396
1437
|
return (0, import_yaml2.parse)(raw) ?? {};
|
|
1397
1438
|
}
|
|
1398
1439
|
|
|
1399
1440
|
// src/utils/handlers.ts
|
|
1400
|
-
var
|
|
1441
|
+
var import_node_fs5 = require("fs");
|
|
1401
1442
|
async function loadHandlers(filePath) {
|
|
1402
|
-
if (!(0,
|
|
1443
|
+
if (!(0, import_node_fs5.existsSync)(filePath)) return /* @__PURE__ */ new Map();
|
|
1403
1444
|
try {
|
|
1404
1445
|
const mod = await import(filePath);
|
|
1405
1446
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1429,8 +1470,12 @@ function registerServe(program2) {
|
|
|
1429
1470
|
).option(
|
|
1430
1471
|
"--handlers <file>",
|
|
1431
1472
|
"Path to a JavaScript file exporting custom route handler functions"
|
|
1473
|
+
).option(
|
|
1474
|
+
"--id-strategy <strategy>",
|
|
1475
|
+
"Id generation strategy for new items: increment (default) or uuid",
|
|
1476
|
+
"increment"
|
|
1432
1477
|
).action(async (file, flags, cmd) => {
|
|
1433
|
-
const fileConfig = loadConfigFile((0,
|
|
1478
|
+
const fileConfig = loadConfigFile((0, import_node_path4.join)(process.cwd(), "yrest.config.yml"));
|
|
1434
1479
|
const cliOverrides = Object.fromEntries(
|
|
1435
1480
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
1436
1481
|
);
|
|
@@ -1449,7 +1494,7 @@ function registerServe(program2) {
|
|
|
1449
1494
|
console.error(`Error: cannot load "${options.file}" \u2014 ${msg}`);
|
|
1450
1495
|
process.exit(1);
|
|
1451
1496
|
}
|
|
1452
|
-
const handlers = options.handlers ? await loadHandlers((0,
|
|
1497
|
+
const handlers = options.handlers ? await loadHandlers((0, import_node_path4.resolve)(options.handlers)) : /* @__PURE__ */ new Map();
|
|
1453
1498
|
const yrestServer = createYrestServerFromStorage(storage, options, handlers);
|
|
1454
1499
|
await yrestServer.start();
|
|
1455
1500
|
const collections = Object.keys(storage.getData());
|
|
@@ -1511,9 +1556,9 @@ function registerServe(program2) {
|
|
|
1511
1556
|
${dim(modes.map((m) => `[${m}]`).join(" "))}`);
|
|
1512
1557
|
console.log("");
|
|
1513
1558
|
if (options.watch) {
|
|
1514
|
-
const absFile = (0,
|
|
1559
|
+
const absFile = (0, import_node_path4.resolve)(options.file);
|
|
1515
1560
|
let debounce;
|
|
1516
|
-
(0,
|
|
1561
|
+
(0, import_node_fs6.watchFile)(absFile, { interval: 300 }, (curr, prev) => {
|
|
1517
1562
|
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
1518
1563
|
clearTimeout(debounce);
|
|
1519
1564
|
debounce = setTimeout(() => {
|
|
@@ -1533,8 +1578,8 @@ function registerServe(program2) {
|
|
|
1533
1578
|
}
|
|
1534
1579
|
|
|
1535
1580
|
// src/cli/commands/handler.ts
|
|
1536
|
-
var
|
|
1537
|
-
var
|
|
1581
|
+
var import_node_fs7 = require("fs");
|
|
1582
|
+
var import_node_path5 = require("path");
|
|
1538
1583
|
var import_yaml3 = require("yaml");
|
|
1539
1584
|
var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
|
|
1540
1585
|
// Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
|
|
@@ -1561,44 +1606,44 @@ function registerHandler(program2) {
|
|
|
1561
1606
|
"--register",
|
|
1562
1607
|
"Also add a _routes entry to db.yml linking this handler to method + path"
|
|
1563
1608
|
).action((name, flags) => {
|
|
1564
|
-
const fileConfig = loadConfigFile((0,
|
|
1565
|
-
const handlersPath = (0,
|
|
1609
|
+
const fileConfig = loadConfigFile((0, import_node_path5.join)(process.cwd(), "yrest.config.yml"));
|
|
1610
|
+
const handlersPath = (0, import_node_path5.resolve)(
|
|
1566
1611
|
fileConfig.handlers ?? "yrest.handlers.js"
|
|
1567
1612
|
);
|
|
1568
|
-
const dbPath = (0,
|
|
1569
|
-
if (!(0,
|
|
1570
|
-
(0,
|
|
1613
|
+
const dbPath = (0, import_node_path5.resolve)(fileConfig.file ?? "db.yml");
|
|
1614
|
+
if (!(0, import_node_fs7.existsSync)(handlersPath)) {
|
|
1615
|
+
(0, import_node_fs7.writeFileSync)(
|
|
1571
1616
|
handlersPath,
|
|
1572
1617
|
HANDLERS_FILE_HEADER + buildStub(name, flags.method, flags.path),
|
|
1573
1618
|
"utf8"
|
|
1574
1619
|
);
|
|
1575
|
-
console.log(` Created ${(0,
|
|
1620
|
+
console.log(` Created ${(0, import_node_path5.basename)(handlersPath)}`);
|
|
1576
1621
|
} else {
|
|
1577
|
-
const existing = (0,
|
|
1622
|
+
const existing = (0, import_node_fs7.readFileSync)(handlersPath, "utf8");
|
|
1578
1623
|
if (existing.includes(`function ${name}(`)) {
|
|
1579
|
-
console.error(` Error: handler "${name}" already exists in ${(0,
|
|
1624
|
+
console.error(` Error: handler "${name}" already exists in ${(0, import_node_path5.basename)(handlersPath)}`);
|
|
1580
1625
|
process.exit(1);
|
|
1581
1626
|
}
|
|
1582
|
-
(0,
|
|
1583
|
-
console.log(` Added handler "${name}" to ${(0,
|
|
1627
|
+
(0, import_node_fs7.appendFileSync)(handlersPath, buildStub(name, flags.method, flags.path), "utf8");
|
|
1628
|
+
console.log(` Added handler "${name}" to ${(0, import_node_path5.basename)(handlersPath)}`);
|
|
1584
1629
|
}
|
|
1585
1630
|
if (flags.register) {
|
|
1586
1631
|
if (!flags.method || !flags.path) {
|
|
1587
1632
|
console.error(" Error: --register requires --method and --path");
|
|
1588
1633
|
process.exit(1);
|
|
1589
1634
|
}
|
|
1590
|
-
if (!(0,
|
|
1635
|
+
if (!(0, import_node_fs7.existsSync)(dbPath)) {
|
|
1591
1636
|
console.error(` Error: database file not found at ${dbPath}`);
|
|
1592
1637
|
process.exit(1);
|
|
1593
1638
|
}
|
|
1594
|
-
const raw = (0, import_yaml3.parse)((0,
|
|
1639
|
+
const raw = (0, import_yaml3.parse)((0, import_node_fs7.readFileSync)(dbPath, "utf8")) ?? {};
|
|
1595
1640
|
if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
|
|
1596
1641
|
const routes = raw["_routes"];
|
|
1597
1642
|
const alreadyRegistered = routes.some((r) => r["handler"] === name);
|
|
1598
1643
|
if (!alreadyRegistered) {
|
|
1599
1644
|
routes.push({ method: flags.method.toUpperCase(), path: flags.path, handler: name });
|
|
1600
|
-
(0,
|
|
1601
|
-
console.log(` Added _routes entry to ${(0,
|
|
1645
|
+
(0, import_node_fs7.writeFileSync)(dbPath, (0, import_yaml3.stringify)(raw), "utf8");
|
|
1646
|
+
console.log(` Added _routes entry to ${(0, import_node_path5.basename)(dbPath)}`);
|
|
1602
1647
|
} else {
|
|
1603
1648
|
console.log(` Handler "${name}" already in _routes \u2014 skipped`);
|
|
1604
1649
|
}
|
|
@@ -1606,10 +1651,10 @@ function registerHandler(program2) {
|
|
|
1606
1651
|
console.log(`
|
|
1607
1652
|
Next steps:`);
|
|
1608
1653
|
if (!fileConfig.handlers) {
|
|
1609
|
-
console.log(` 1. Add handlers: ${(0,
|
|
1610
|
-
console.log(` 2. Implement the "${name}" function in ${(0,
|
|
1654
|
+
console.log(` 1. Add handlers: ${(0, import_node_path5.basename)(handlersPath)} to yrest.config.yml`);
|
|
1655
|
+
console.log(` 2. Implement the "${name}" function in ${(0, import_node_path5.basename)(handlersPath)}`);
|
|
1611
1656
|
} else {
|
|
1612
|
-
console.log(` 1. Implement the "${name}" function in ${(0,
|
|
1657
|
+
console.log(` 1. Implement the "${name}" function in ${(0, import_node_path5.basename)(handlersPath)}`);
|
|
1613
1658
|
}
|
|
1614
1659
|
if (!flags.register) {
|
|
1615
1660
|
console.log(
|
package/dist/cli/index.mjs
CHANGED
|
@@ -106,7 +106,7 @@ function registerInit(program2) {
|
|
|
106
106
|
|
|
107
107
|
// src/cli/commands/serve.ts
|
|
108
108
|
import { watchFile } from "fs";
|
|
109
|
-
import { join, resolve as resolve3 } from "path";
|
|
109
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
110
110
|
|
|
111
111
|
// src/storage/yrestStorage.ts
|
|
112
112
|
import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
@@ -196,6 +196,11 @@ function createYrestStorage(filePath) {
|
|
|
196
196
|
import Fastify from "fastify";
|
|
197
197
|
import cors from "@fastify/cors";
|
|
198
198
|
|
|
199
|
+
// src/router/templates/about.template.ts
|
|
200
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
201
|
+
import { dirname as dirname2, join } from "path";
|
|
202
|
+
import { fileURLToPath } from "url";
|
|
203
|
+
|
|
199
204
|
// src/utils/interpolate.ts
|
|
200
205
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
201
206
|
function getPath(obj, path) {
|
|
@@ -243,6 +248,15 @@ function hasTemplates(value) {
|
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
// src/router/templates/about.template.ts
|
|
251
|
+
var _dir = dirname2(fileURLToPath(import.meta.url));
|
|
252
|
+
var LOGO_SRC = (() => {
|
|
253
|
+
try {
|
|
254
|
+
const buf = readFileSync2(join(_dir, "../../assets/logo-color.png"));
|
|
255
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
256
|
+
} catch {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
246
260
|
var METHOD_COLOR = {
|
|
247
261
|
GET: "#3fb950",
|
|
248
262
|
POST: "#58a6ff",
|
|
@@ -401,6 +415,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
401
415
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
402
416
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
403
417
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
418
|
+
if (options.idStrategy !== "increment")
|
|
419
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
404
420
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
405
421
|
const nestedRows = [];
|
|
406
422
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -443,6 +459,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
443
459
|
${customRoutes.map((r) => {
|
|
444
460
|
const fullPath = `${base}${r.path}`;
|
|
445
461
|
const tags = [];
|
|
462
|
+
if (r.error) {
|
|
463
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
464
|
+
}
|
|
446
465
|
if (r.delay && r.delay > 0) {
|
|
447
466
|
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
448
467
|
}
|
|
@@ -456,7 +475,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
456
475
|
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
457
476
|
}
|
|
458
477
|
let desc;
|
|
459
|
-
if (r.
|
|
478
|
+
if (r.error) {
|
|
479
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
480
|
+
} else if (r.handler) {
|
|
460
481
|
const found = handlers.has(r.handler);
|
|
461
482
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
462
483
|
} else if (r.scenarios?.length) {
|
|
@@ -620,7 +641,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
620
641
|
|
|
621
642
|
<div class="banner">
|
|
622
643
|
<div class="banner-inner">
|
|
623
|
-
|
|
644
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
624
645
|
<p>Zero-config REST API mock server</p>
|
|
625
646
|
<div class="banner-meta">
|
|
626
647
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -703,6 +724,10 @@ function nextId(items) {
|
|
|
703
724
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
704
725
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
705
726
|
}
|
|
727
|
+
function generateId(items, strategy) {
|
|
728
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
729
|
+
return nextId(items);
|
|
730
|
+
}
|
|
706
731
|
function firstParam(value) {
|
|
707
732
|
if (value === void 0) return void 0;
|
|
708
733
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -790,10 +815,10 @@ function findById(items, id) {
|
|
|
790
815
|
function findIndexById(items, id) {
|
|
791
816
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
792
817
|
}
|
|
793
|
-
function createItem(storage, resource, body) {
|
|
818
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
794
819
|
const collection = storage.getCollection(resource) ?? [];
|
|
795
820
|
const item = {
|
|
796
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
821
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
797
822
|
...body
|
|
798
823
|
};
|
|
799
824
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -971,7 +996,12 @@ var CollectionRouteCommand = class {
|
|
|
971
996
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
972
997
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
973
998
|
}
|
|
974
|
-
const item = createItem(
|
|
999
|
+
const item = createItem(
|
|
1000
|
+
this.storage,
|
|
1001
|
+
this.resource,
|
|
1002
|
+
req.body,
|
|
1003
|
+
this.options.idStrategy
|
|
1004
|
+
);
|
|
975
1005
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
976
1006
|
});
|
|
977
1007
|
}
|
|
@@ -1061,6 +1091,10 @@ var CustomRouteCommand = class {
|
|
|
1061
1091
|
if (route.delay && route.delay > 0) {
|
|
1062
1092
|
await new Promise((resolve5) => setTimeout(resolve5, route.delay));
|
|
1063
1093
|
}
|
|
1094
|
+
if (route.error) {
|
|
1095
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1096
|
+
return reply.status(route.error).send(body2);
|
|
1097
|
+
}
|
|
1064
1098
|
for (const [key, value] of Object.entries(headers)) {
|
|
1065
1099
|
reply.header(key, value);
|
|
1066
1100
|
}
|
|
@@ -1357,15 +1391,22 @@ var yrestOptionsSchema = z.object({
|
|
|
1357
1391
|
pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1358
1392
|
enabled: v !== false,
|
|
1359
1393
|
limit: v === false || v === true ? 10 : v
|
|
1360
|
-
}))
|
|
1394
|
+
})),
|
|
1395
|
+
/**
|
|
1396
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1397
|
+
*
|
|
1398
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1399
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1400
|
+
*/
|
|
1401
|
+
idStrategy: z.enum(["increment", "uuid"]).default("increment")
|
|
1361
1402
|
});
|
|
1362
1403
|
|
|
1363
1404
|
// src/config/loadConfigFile.ts
|
|
1364
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1405
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1365
1406
|
import { parse as parse2 } from "yaml";
|
|
1366
1407
|
function loadConfigFile(configPath) {
|
|
1367
1408
|
if (!existsSync2(configPath)) return {};
|
|
1368
|
-
const raw =
|
|
1409
|
+
const raw = readFileSync3(configPath, "utf8");
|
|
1369
1410
|
return parse2(raw) ?? {};
|
|
1370
1411
|
}
|
|
1371
1412
|
|
|
@@ -1402,8 +1443,12 @@ function registerServe(program2) {
|
|
|
1402
1443
|
).option(
|
|
1403
1444
|
"--handlers <file>",
|
|
1404
1445
|
"Path to a JavaScript file exporting custom route handler functions"
|
|
1446
|
+
).option(
|
|
1447
|
+
"--id-strategy <strategy>",
|
|
1448
|
+
"Id generation strategy for new items: increment (default) or uuid",
|
|
1449
|
+
"increment"
|
|
1405
1450
|
).action(async (file, flags, cmd) => {
|
|
1406
|
-
const fileConfig = loadConfigFile(
|
|
1451
|
+
const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
|
|
1407
1452
|
const cliOverrides = Object.fromEntries(
|
|
1408
1453
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
1409
1454
|
);
|
|
@@ -1506,8 +1551,8 @@ function registerServe(program2) {
|
|
|
1506
1551
|
}
|
|
1507
1552
|
|
|
1508
1553
|
// src/cli/commands/handler.ts
|
|
1509
|
-
import { existsSync as existsSync4, readFileSync as
|
|
1510
|
-
import { join as
|
|
1554
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1555
|
+
import { join as join3, resolve as resolve4, basename } from "path";
|
|
1511
1556
|
import { parse as parse3, stringify as stringify2 } from "yaml";
|
|
1512
1557
|
var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
|
|
1513
1558
|
// Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
|
|
@@ -1534,7 +1579,7 @@ function registerHandler(program2) {
|
|
|
1534
1579
|
"--register",
|
|
1535
1580
|
"Also add a _routes entry to db.yml linking this handler to method + path"
|
|
1536
1581
|
).action((name, flags) => {
|
|
1537
|
-
const fileConfig = loadConfigFile(
|
|
1582
|
+
const fileConfig = loadConfigFile(join3(process.cwd(), "yrest.config.yml"));
|
|
1538
1583
|
const handlersPath = resolve4(
|
|
1539
1584
|
fileConfig.handlers ?? "yrest.handlers.js"
|
|
1540
1585
|
);
|
|
@@ -1547,7 +1592,7 @@ function registerHandler(program2) {
|
|
|
1547
1592
|
);
|
|
1548
1593
|
console.log(` Created ${basename(handlersPath)}`);
|
|
1549
1594
|
} else {
|
|
1550
|
-
const existing =
|
|
1595
|
+
const existing = readFileSync4(handlersPath, "utf8");
|
|
1551
1596
|
if (existing.includes(`function ${name}(`)) {
|
|
1552
1597
|
console.error(` Error: handler "${name}" already exists in ${basename(handlersPath)}`);
|
|
1553
1598
|
process.exit(1);
|
|
@@ -1564,7 +1609,7 @@ function registerHandler(program2) {
|
|
|
1564
1609
|
console.error(` Error: database file not found at ${dbPath}`);
|
|
1565
1610
|
process.exit(1);
|
|
1566
1611
|
}
|
|
1567
|
-
const raw = parse3(
|
|
1612
|
+
const raw = parse3(readFileSync4(dbPath, "utf8")) ?? {};
|
|
1568
1613
|
if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
|
|
1569
1614
|
const routes = raw["_routes"];
|
|
1570
1615
|
const alreadyRegistered = routes.some((r) => r["handler"] === name);
|
package/dist/index.d.mts
CHANGED
|
@@ -112,6 +112,19 @@ type CustomRoute = {
|
|
|
112
112
|
* Applied before any response is sent, regardless of which path resolved the response.
|
|
113
113
|
*/
|
|
114
114
|
delay?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Forces this route to always return the given HTTP status code as an error,
|
|
117
|
+
* bypassing handlers, scenarios and the static response entirely.
|
|
118
|
+
* Applied after `delay:` so slow-error scenarios still work.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Always return 503
|
|
122
|
+
* error: 503
|
|
123
|
+
* errorBody: { message: "Service unavailable" }
|
|
124
|
+
*/
|
|
125
|
+
error?: number;
|
|
126
|
+
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
127
|
+
errorBody?: unknown;
|
|
115
128
|
};
|
|
116
129
|
/**
|
|
117
130
|
* In-memory store backed by a YAML file.
|
|
@@ -251,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
251
264
|
enabled: boolean;
|
|
252
265
|
limit: number;
|
|
253
266
|
}, number | boolean | undefined>;
|
|
267
|
+
/**
|
|
268
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
269
|
+
*
|
|
270
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
271
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
272
|
+
*/
|
|
273
|
+
idStrategy: z.ZodDefault<z.ZodEnum<["increment", "uuid"]>>;
|
|
254
274
|
}, "strip", z.ZodTypeAny, {
|
|
255
275
|
file: string;
|
|
256
276
|
port: number;
|
|
@@ -264,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
264
284
|
enabled: boolean;
|
|
265
285
|
limit: number;
|
|
266
286
|
};
|
|
287
|
+
idStrategy: "increment" | "uuid";
|
|
267
288
|
handlers?: string | undefined;
|
|
268
289
|
}, {
|
|
269
290
|
file: string;
|
|
@@ -276,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
276
297
|
delay?: number | undefined;
|
|
277
298
|
handlers?: string | undefined;
|
|
278
299
|
pageable?: number | boolean | undefined;
|
|
300
|
+
idStrategy?: "increment" | "uuid" | undefined;
|
|
279
301
|
}>;
|
|
280
302
|
/**
|
|
281
303
|
* Resolved server configuration after Zod validation and transformation.
|
package/dist/index.d.ts
CHANGED
|
@@ -112,6 +112,19 @@ type CustomRoute = {
|
|
|
112
112
|
* Applied before any response is sent, regardless of which path resolved the response.
|
|
113
113
|
*/
|
|
114
114
|
delay?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Forces this route to always return the given HTTP status code as an error,
|
|
117
|
+
* bypassing handlers, scenarios and the static response entirely.
|
|
118
|
+
* Applied after `delay:` so slow-error scenarios still work.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Always return 503
|
|
122
|
+
* error: 503
|
|
123
|
+
* errorBody: { message: "Service unavailable" }
|
|
124
|
+
*/
|
|
125
|
+
error?: number;
|
|
126
|
+
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
127
|
+
errorBody?: unknown;
|
|
115
128
|
};
|
|
116
129
|
/**
|
|
117
130
|
* In-memory store backed by a YAML file.
|
|
@@ -251,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
251
264
|
enabled: boolean;
|
|
252
265
|
limit: number;
|
|
253
266
|
}, number | boolean | undefined>;
|
|
267
|
+
/**
|
|
268
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
269
|
+
*
|
|
270
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
271
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
272
|
+
*/
|
|
273
|
+
idStrategy: z.ZodDefault<z.ZodEnum<["increment", "uuid"]>>;
|
|
254
274
|
}, "strip", z.ZodTypeAny, {
|
|
255
275
|
file: string;
|
|
256
276
|
port: number;
|
|
@@ -264,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
264
284
|
enabled: boolean;
|
|
265
285
|
limit: number;
|
|
266
286
|
};
|
|
287
|
+
idStrategy: "increment" | "uuid";
|
|
267
288
|
handlers?: string | undefined;
|
|
268
289
|
}, {
|
|
269
290
|
file: string;
|
|
@@ -276,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
276
297
|
delay?: number | undefined;
|
|
277
298
|
handlers?: string | undefined;
|
|
278
299
|
pageable?: number | boolean | undefined;
|
|
300
|
+
idStrategy?: "increment" | "uuid" | undefined;
|
|
279
301
|
}>;
|
|
280
302
|
/**
|
|
281
303
|
* Resolved server configuration after Zod validation and transformation.
|
package/dist/index.js
CHANGED
|
@@ -31,9 +31,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var getImportMetaUrl, importMetaUrl;
|
|
34
35
|
var init_cjs_shims = __esm({
|
|
35
36
|
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
36
37
|
"use strict";
|
|
38
|
+
getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
39
|
+
importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
37
40
|
}
|
|
38
41
|
});
|
|
39
42
|
|
|
@@ -56,8 +59,8 @@ __export(yrestStorage_exports, {
|
|
|
56
59
|
createYrestStorage: () => createYrestStorage
|
|
57
60
|
});
|
|
58
61
|
function createYrestStorage(filePath) {
|
|
59
|
-
const absPath = (0,
|
|
60
|
-
const raw = (0, import_yaml2.parse)((0,
|
|
62
|
+
const absPath = (0, import_node_path2.resolve)(filePath);
|
|
63
|
+
const raw = (0, import_yaml2.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
61
64
|
const relations = raw["_rel"] ?? {};
|
|
62
65
|
const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
|
|
63
66
|
const data = Object.fromEntries(
|
|
@@ -89,12 +92,12 @@ function createYrestStorage(filePath) {
|
|
|
89
92
|
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
90
93
|
if (routes.length > 0) payload._routes = routes;
|
|
91
94
|
Object.assign(payload, data);
|
|
92
|
-
const tmp = (0,
|
|
93
|
-
(0,
|
|
94
|
-
(0,
|
|
95
|
+
const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
|
|
96
|
+
(0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml2.stringify)(payload), "utf8");
|
|
97
|
+
(0, import_node_fs3.renameSync)(tmp, absPath);
|
|
95
98
|
},
|
|
96
99
|
reload() {
|
|
97
|
-
const fresh = (0, import_yaml2.parse)((0,
|
|
100
|
+
const fresh = (0, import_yaml2.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
98
101
|
const freshRelations = fresh["_rel"] ?? {};
|
|
99
102
|
const freshData = Object.fromEntries(
|
|
100
103
|
Object.entries(fresh).filter(([key]) => key !== "_rel" && key !== "_routes")
|
|
@@ -124,13 +127,13 @@ function createYrestStorage(filePath) {
|
|
|
124
127
|
}
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
|
-
var
|
|
130
|
+
var import_node_fs3, import_node_path2, import_node_crypto2, import_yaml2;
|
|
128
131
|
var init_yrestStorage = __esm({
|
|
129
132
|
"src/storage/yrestStorage.ts"() {
|
|
130
133
|
"use strict";
|
|
131
134
|
init_cjs_shims();
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
import_node_fs3 = require("fs");
|
|
136
|
+
import_node_path2 = require("path");
|
|
134
137
|
import_node_crypto2 = require("crypto");
|
|
135
138
|
import_yaml2 = require("yaml");
|
|
136
139
|
init_deepCopy();
|
|
@@ -194,7 +197,7 @@ function dedent(str) {
|
|
|
194
197
|
|
|
195
198
|
// src/api/yrestServer.ts
|
|
196
199
|
init_cjs_shims();
|
|
197
|
-
var
|
|
200
|
+
var import_node_path3 = require("path");
|
|
198
201
|
|
|
199
202
|
// src/utils/handlers.ts
|
|
200
203
|
init_cjs_shims();
|
|
@@ -237,6 +240,9 @@ init_cjs_shims();
|
|
|
237
240
|
|
|
238
241
|
// src/router/templates/about.template.ts
|
|
239
242
|
init_cjs_shims();
|
|
243
|
+
var import_node_fs2 = require("fs");
|
|
244
|
+
var import_node_path = require("path");
|
|
245
|
+
var import_node_url = require("url");
|
|
240
246
|
|
|
241
247
|
// src/utils/interpolate.ts
|
|
242
248
|
init_cjs_shims();
|
|
@@ -286,6 +292,15 @@ function hasTemplates(value) {
|
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
// src/router/templates/about.template.ts
|
|
295
|
+
var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
296
|
+
var LOGO_SRC = (() => {
|
|
297
|
+
try {
|
|
298
|
+
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/logo-color.png"));
|
|
299
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
300
|
+
} catch {
|
|
301
|
+
return "";
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
289
304
|
var METHOD_COLOR = {
|
|
290
305
|
GET: "#3fb950",
|
|
291
306
|
POST: "#58a6ff",
|
|
@@ -444,6 +459,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
444
459
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
445
460
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
446
461
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
462
|
+
if (options.idStrategy !== "increment")
|
|
463
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
447
464
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
448
465
|
const nestedRows = [];
|
|
449
466
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -486,6 +503,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
486
503
|
${customRoutes.map((r) => {
|
|
487
504
|
const fullPath = `${base}${r.path}`;
|
|
488
505
|
const tags = [];
|
|
506
|
+
if (r.error) {
|
|
507
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
508
|
+
}
|
|
489
509
|
if (r.delay && r.delay > 0) {
|
|
490
510
|
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
491
511
|
}
|
|
@@ -499,7 +519,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
499
519
|
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
500
520
|
}
|
|
501
521
|
let desc;
|
|
502
|
-
if (r.
|
|
522
|
+
if (r.error) {
|
|
523
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
524
|
+
} else if (r.handler) {
|
|
503
525
|
const found = handlers.has(r.handler);
|
|
504
526
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
505
527
|
} else if (r.scenarios?.length) {
|
|
@@ -663,7 +685,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
663
685
|
|
|
664
686
|
<div class="banner">
|
|
665
687
|
<div class="banner-inner">
|
|
666
|
-
|
|
688
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
667
689
|
<p>Zero-config REST API mock server</p>
|
|
668
690
|
<div class="banner-meta">
|
|
669
691
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -750,6 +772,10 @@ function nextId(items) {
|
|
|
750
772
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
751
773
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
752
774
|
}
|
|
775
|
+
function generateId(items, strategy) {
|
|
776
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
777
|
+
return nextId(items);
|
|
778
|
+
}
|
|
753
779
|
function firstParam(value) {
|
|
754
780
|
if (value === void 0) return void 0;
|
|
755
781
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -839,10 +865,10 @@ function findById(items, id) {
|
|
|
839
865
|
function findIndexById(items, id) {
|
|
840
866
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
841
867
|
}
|
|
842
|
-
function createItem(storage, resource, body) {
|
|
868
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
843
869
|
const collection = storage.getCollection(resource) ?? [];
|
|
844
870
|
const item = {
|
|
845
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
871
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
846
872
|
...body
|
|
847
873
|
};
|
|
848
874
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -1021,7 +1047,12 @@ var CollectionRouteCommand = class {
|
|
|
1021
1047
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
1022
1048
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
1023
1049
|
}
|
|
1024
|
-
const item = createItem(
|
|
1050
|
+
const item = createItem(
|
|
1051
|
+
this.storage,
|
|
1052
|
+
this.resource,
|
|
1053
|
+
req.body,
|
|
1054
|
+
this.options.idStrategy
|
|
1055
|
+
);
|
|
1025
1056
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
1026
1057
|
});
|
|
1027
1058
|
}
|
|
@@ -1115,6 +1146,10 @@ var CustomRouteCommand = class {
|
|
|
1115
1146
|
if (route.delay && route.delay > 0) {
|
|
1116
1147
|
await new Promise((resolve3) => setTimeout(resolve3, route.delay));
|
|
1117
1148
|
}
|
|
1149
|
+
if (route.error) {
|
|
1150
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1151
|
+
return reply.status(route.error).send(body2);
|
|
1152
|
+
}
|
|
1118
1153
|
for (const [key, value] of Object.entries(headers)) {
|
|
1119
1154
|
reply.header(key, value);
|
|
1120
1155
|
}
|
|
@@ -1385,7 +1420,7 @@ function createYrestServer(options) {
|
|
|
1385
1420
|
return {
|
|
1386
1421
|
async start() {
|
|
1387
1422
|
const storage = "data" in options && options.data !== void 0 ? createInMemoryStorage(options.data) : (await Promise.resolve().then(() => (init_yrestStorage(), yrestStorage_exports))).createYrestStorage(resolvedOptions.file);
|
|
1388
|
-
const handlers = resolvedOptions.handlers ? await loadHandlers((0,
|
|
1423
|
+
const handlers = resolvedOptions.handlers ? await loadHandlers((0, import_node_path3.resolve)(resolvedOptions.handlers)) : /* @__PURE__ */ new Map();
|
|
1389
1424
|
_inner = createYrestServerFromStorage(storage, resolvedOptions, handlers);
|
|
1390
1425
|
await _inner.start();
|
|
1391
1426
|
},
|
|
@@ -1412,7 +1447,8 @@ function buildOptions(opts) {
|
|
|
1412
1447
|
readonly: opts.readonly ?? false,
|
|
1413
1448
|
delay: opts.delay ?? 0,
|
|
1414
1449
|
handlers: opts.handlers,
|
|
1415
|
-
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
|
|
1450
|
+
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
|
|
1451
|
+
idStrategy: "increment"
|
|
1416
1452
|
};
|
|
1417
1453
|
}
|
|
1418
1454
|
function createInMemoryStorage(data) {
|
|
@@ -1496,7 +1532,14 @@ var yrestOptionsSchema = import_zod.z.object({
|
|
|
1496
1532
|
pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1497
1533
|
enabled: v !== false,
|
|
1498
1534
|
limit: v === false || v === true ? 10 : v
|
|
1499
|
-
}))
|
|
1535
|
+
})),
|
|
1536
|
+
/**
|
|
1537
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1538
|
+
*
|
|
1539
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1540
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1541
|
+
*/
|
|
1542
|
+
idStrategy: import_zod.z.enum(["increment", "uuid"]).default("increment")
|
|
1500
1543
|
});
|
|
1501
1544
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1502
1545
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -35,13 +35,13 @@ var yrestStorage_exports = {};
|
|
|
35
35
|
__export(yrestStorage_exports, {
|
|
36
36
|
createYrestStorage: () => createYrestStorage
|
|
37
37
|
});
|
|
38
|
-
import { readFileSync, writeFileSync, renameSync } from "fs";
|
|
39
|
-
import { resolve, dirname } from "path";
|
|
38
|
+
import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
39
|
+
import { resolve, dirname as dirname2 } from "path";
|
|
40
40
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
41
41
|
import { parse as parse2, stringify } from "yaml";
|
|
42
42
|
function createYrestStorage(filePath) {
|
|
43
43
|
const absPath = resolve(filePath);
|
|
44
|
-
const raw = parse2(
|
|
44
|
+
const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
|
|
45
45
|
const relations = raw["_rel"] ?? {};
|
|
46
46
|
const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
|
|
47
47
|
const data = Object.fromEntries(
|
|
@@ -73,12 +73,12 @@ function createYrestStorage(filePath) {
|
|
|
73
73
|
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
74
74
|
if (routes.length > 0) payload._routes = routes;
|
|
75
75
|
Object.assign(payload, data);
|
|
76
|
-
const tmp = resolve(
|
|
76
|
+
const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
|
|
77
77
|
writeFileSync(tmp, stringify(payload), "utf8");
|
|
78
78
|
renameSync(tmp, absPath);
|
|
79
79
|
},
|
|
80
80
|
reload() {
|
|
81
|
-
const fresh = parse2(
|
|
81
|
+
const fresh = parse2(readFileSync2(absPath, "utf8")) ?? {};
|
|
82
82
|
const freshRelations = fresh["_rel"] ?? {};
|
|
83
83
|
const freshData = Object.fromEntries(
|
|
84
84
|
Object.entries(fresh).filter(([key]) => key !== "_rel" && key !== "_routes")
|
|
@@ -206,6 +206,9 @@ init_esm_shims();
|
|
|
206
206
|
|
|
207
207
|
// src/router/templates/about.template.ts
|
|
208
208
|
init_esm_shims();
|
|
209
|
+
import { readFileSync } from "fs";
|
|
210
|
+
import { dirname, join } from "path";
|
|
211
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
209
212
|
|
|
210
213
|
// src/utils/interpolate.ts
|
|
211
214
|
init_esm_shims();
|
|
@@ -255,6 +258,15 @@ function hasTemplates(value) {
|
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
// src/router/templates/about.template.ts
|
|
261
|
+
var _dir = dirname(fileURLToPath2(import.meta.url));
|
|
262
|
+
var LOGO_SRC = (() => {
|
|
263
|
+
try {
|
|
264
|
+
const buf = readFileSync(join(_dir, "../../assets/logo-color.png"));
|
|
265
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
266
|
+
} catch {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
258
270
|
var METHOD_COLOR = {
|
|
259
271
|
GET: "#3fb950",
|
|
260
272
|
POST: "#58a6ff",
|
|
@@ -413,6 +425,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
413
425
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
414
426
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
415
427
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
428
|
+
if (options.idStrategy !== "increment")
|
|
429
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
416
430
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
417
431
|
const nestedRows = [];
|
|
418
432
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -455,6 +469,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
455
469
|
${customRoutes.map((r) => {
|
|
456
470
|
const fullPath = `${base}${r.path}`;
|
|
457
471
|
const tags = [];
|
|
472
|
+
if (r.error) {
|
|
473
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
474
|
+
}
|
|
458
475
|
if (r.delay && r.delay > 0) {
|
|
459
476
|
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
460
477
|
}
|
|
@@ -468,7 +485,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
468
485
|
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
469
486
|
}
|
|
470
487
|
let desc;
|
|
471
|
-
if (r.
|
|
488
|
+
if (r.error) {
|
|
489
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
490
|
+
} else if (r.handler) {
|
|
472
491
|
const found = handlers.has(r.handler);
|
|
473
492
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
474
493
|
} else if (r.scenarios?.length) {
|
|
@@ -632,7 +651,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
632
651
|
|
|
633
652
|
<div class="banner">
|
|
634
653
|
<div class="banner-inner">
|
|
635
|
-
|
|
654
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
636
655
|
<p>Zero-config REST API mock server</p>
|
|
637
656
|
<div class="banner-meta">
|
|
638
657
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -719,6 +738,10 @@ function nextId(items) {
|
|
|
719
738
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
720
739
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
721
740
|
}
|
|
741
|
+
function generateId(items, strategy) {
|
|
742
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
743
|
+
return nextId(items);
|
|
744
|
+
}
|
|
722
745
|
function firstParam(value) {
|
|
723
746
|
if (value === void 0) return void 0;
|
|
724
747
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -808,10 +831,10 @@ function findById(items, id) {
|
|
|
808
831
|
function findIndexById(items, id) {
|
|
809
832
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
810
833
|
}
|
|
811
|
-
function createItem(storage, resource, body) {
|
|
834
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
812
835
|
const collection = storage.getCollection(resource) ?? [];
|
|
813
836
|
const item = {
|
|
814
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
837
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
815
838
|
...body
|
|
816
839
|
};
|
|
817
840
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -990,7 +1013,12 @@ var CollectionRouteCommand = class {
|
|
|
990
1013
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
991
1014
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
992
1015
|
}
|
|
993
|
-
const item = createItem(
|
|
1016
|
+
const item = createItem(
|
|
1017
|
+
this.storage,
|
|
1018
|
+
this.resource,
|
|
1019
|
+
req.body,
|
|
1020
|
+
this.options.idStrategy
|
|
1021
|
+
);
|
|
994
1022
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
995
1023
|
});
|
|
996
1024
|
}
|
|
@@ -1084,6 +1112,10 @@ var CustomRouteCommand = class {
|
|
|
1084
1112
|
if (route.delay && route.delay > 0) {
|
|
1085
1113
|
await new Promise((resolve3) => setTimeout(resolve3, route.delay));
|
|
1086
1114
|
}
|
|
1115
|
+
if (route.error) {
|
|
1116
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1117
|
+
return reply.status(route.error).send(body2);
|
|
1118
|
+
}
|
|
1087
1119
|
for (const [key, value] of Object.entries(headers)) {
|
|
1088
1120
|
reply.header(key, value);
|
|
1089
1121
|
}
|
|
@@ -1381,7 +1413,8 @@ function buildOptions(opts) {
|
|
|
1381
1413
|
readonly: opts.readonly ?? false,
|
|
1382
1414
|
delay: opts.delay ?? 0,
|
|
1383
1415
|
handlers: opts.handlers,
|
|
1384
|
-
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
|
|
1416
|
+
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
|
|
1417
|
+
idStrategy: "increment"
|
|
1385
1418
|
};
|
|
1386
1419
|
}
|
|
1387
1420
|
function createInMemoryStorage(data) {
|
|
@@ -1465,7 +1498,14 @@ var yrestOptionsSchema = z.object({
|
|
|
1465
1498
|
pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1466
1499
|
enabled: v !== false,
|
|
1467
1500
|
limit: v === false || v === true ? 10 : v
|
|
1468
|
-
}))
|
|
1501
|
+
})),
|
|
1502
|
+
/**
|
|
1503
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1504
|
+
*
|
|
1505
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1506
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1507
|
+
*/
|
|
1508
|
+
idStrategy: z.enum(["increment", "uuid"]).default("increment")
|
|
1469
1509
|
});
|
|
1470
1510
|
export {
|
|
1471
1511
|
createServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yrest/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"yrest": "./dist/cli/index.js"
|
|
53
53
|
},
|
|
54
54
|
"files": [
|
|
55
|
-
"dist"
|
|
55
|
+
"dist",
|
|
56
|
+
"assets"
|
|
56
57
|
],
|
|
57
58
|
"scripts": {
|
|
58
59
|
"build": "tsup",
|
|
@@ -64,7 +65,8 @@
|
|
|
64
65
|
"lint:fix": "eslint src tests --fix",
|
|
65
66
|
"format": "prettier --write .",
|
|
66
67
|
"format:check": "prettier --check .",
|
|
67
|
-
"prepublishOnly": "npm run test:run && npm run build"
|
|
68
|
+
"prepublishOnly": "npm run test:run && npm run build",
|
|
69
|
+
"version": "node scripts/update-version-badge.mjs && git add README.md"
|
|
68
70
|
},
|
|
69
71
|
"dependencies": {
|
|
70
72
|
"@fastify/cors": "^10.0.0",
|
|
@@ -86,5 +88,10 @@
|
|
|
86
88
|
},
|
|
87
89
|
"engines": {
|
|
88
90
|
"node": ">=20"
|
|
91
|
+
},
|
|
92
|
+
"socket": {
|
|
93
|
+
"allow": {
|
|
94
|
+
"filesystem": true
|
|
95
|
+
}
|
|
89
96
|
}
|
|
90
97
|
}
|