@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 CHANGED
@@ -1,4 +1,8 @@
1
- # yRest
1
+ <div align="center">
2
+ <img src="assets/yRest-banner.png" alt="yRest" width="100%" />
3
+ </div>
4
+
5
+ <br/>
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
4
8
  [![npm downloads](https://img.shields.io/npm/dw/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
@@ -6,6 +10,7 @@
6
10
  [![CI](https://github.com/aggiovato/yRest/actions/workflows/ci.yml/badge.svg)](https://github.com/aggiovato/yRest/actions)
7
11
  [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
8
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
13
+ [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.8.0)](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 import_node_fs5 = require("fs");
136
- var import_node_path3 = require("path");
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.handler) {
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
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
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"] : nextId(collection),
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(this.storage, this.resource, req.body);
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 import_node_fs3 = require("fs");
1432
+ var import_node_fs4 = require("fs");
1392
1433
  var import_yaml2 = require("yaml");
1393
1434
  function loadConfigFile(configPath) {
1394
- if (!(0, import_node_fs3.existsSync)(configPath)) return {};
1395
- const raw = (0, import_node_fs3.readFileSync)(configPath, "utf8");
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 import_node_fs4 = require("fs");
1441
+ var import_node_fs5 = require("fs");
1401
1442
  async function loadHandlers(filePath) {
1402
- if (!(0, import_node_fs4.existsSync)(filePath)) return /* @__PURE__ */ new Map();
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, import_node_path3.join)(process.cwd(), "yrest.config.yml"));
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, import_node_path3.resolve)(options.handlers)) : /* @__PURE__ */ new Map();
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, import_node_path3.resolve)(options.file);
1559
+ const absFile = (0, import_node_path4.resolve)(options.file);
1515
1560
  let debounce;
1516
- (0, import_node_fs5.watchFile)(absFile, { interval: 300 }, (curr, prev) => {
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 import_node_fs6 = require("fs");
1537
- var import_node_path4 = require("path");
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, import_node_path4.join)(process.cwd(), "yrest.config.yml"));
1565
- const handlersPath = (0, import_node_path4.resolve)(
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, import_node_path4.resolve)(fileConfig.file ?? "db.yml");
1569
- if (!(0, import_node_fs6.existsSync)(handlersPath)) {
1570
- (0, import_node_fs6.writeFileSync)(
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, import_node_path4.basename)(handlersPath)}`);
1620
+ console.log(` Created ${(0, import_node_path5.basename)(handlersPath)}`);
1576
1621
  } else {
1577
- const existing = (0, import_node_fs6.readFileSync)(handlersPath, "utf8");
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, import_node_path4.basename)(handlersPath)}`);
1624
+ console.error(` Error: handler "${name}" already exists in ${(0, import_node_path5.basename)(handlersPath)}`);
1580
1625
  process.exit(1);
1581
1626
  }
1582
- (0, import_node_fs6.appendFileSync)(handlersPath, buildStub(name, flags.method, flags.path), "utf8");
1583
- console.log(` Added handler "${name}" to ${(0, import_node_path4.basename)(handlersPath)}`);
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, import_node_fs6.existsSync)(dbPath)) {
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, import_node_fs6.readFileSync)(dbPath, "utf8")) ?? {};
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, import_node_fs6.writeFileSync)(dbPath, (0, import_yaml3.stringify)(raw), "utf8");
1601
- console.log(` Added _routes entry to ${(0, import_node_path4.basename)(dbPath)}`);
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, import_node_path4.basename)(handlersPath)} to yrest.config.yml`);
1610
- console.log(` 2. Implement the "${name}" function in ${(0, import_node_path4.basename)(handlersPath)}`);
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, import_node_path4.basename)(handlersPath)}`);
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(
@@ -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.handler) {
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
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
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"] : nextId(collection),
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(this.storage, this.resource, req.body);
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 readFileSync2 } from "fs";
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 = readFileSync2(configPath, "utf8");
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(join(process.cwd(), "yrest.config.yml"));
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 readFileSync3, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
1510
- import { join as join2, resolve as resolve4, basename } from "path";
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(join2(process.cwd(), "yrest.config.yml"));
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 = readFileSync3(handlersPath, "utf8");
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(readFileSync3(dbPath, "utf8")) ?? {};
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, import_node_path.resolve)(filePath);
60
- const raw = (0, import_yaml2.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
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, import_node_path.resolve)((0, import_node_path.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
93
- (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml2.stringify)(payload), "utf8");
94
- (0, import_node_fs2.renameSync)(tmp, absPath);
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, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
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 import_node_fs2, import_node_path, import_node_crypto2, import_yaml2;
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
- import_node_fs2 = require("fs");
133
- import_node_path = require("path");
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 import_node_path2 = require("path");
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.handler) {
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
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
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"] : nextId(collection),
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(this.storage, this.resource, req.body);
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, import_node_path2.resolve)(resolvedOptions.handlers)) : /* @__PURE__ */ new Map();
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(readFileSync(absPath, "utf8")) ?? {};
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(dirname(absPath), `.yrest-${randomUUID2()}.tmp`);
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(readFileSync(absPath, "utf8")) ?? {};
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.handler) {
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
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
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"] : nextId(collection),
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(this.storage, this.resource, req.body);
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.7.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
  }