@yrest/cli 0.6.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/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)) {
@@ -485,16 +502,38 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
485
502
  <table><tbody>
486
503
  ${customRoutes.map((r) => {
487
504
  const fullPath = `${base}${r.path}`;
505
+ const tags = [];
506
+ if (r.error) {
507
+ tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
508
+ }
509
+ if (r.delay && r.delay > 0) {
510
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
511
+ }
512
+ if (r.scenarios?.length) {
513
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
514
+ tags.push(
515
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
516
+ );
517
+ }
518
+ if (r.otherwise) {
519
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
520
+ }
488
521
  let desc;
489
- if (r.handler) {
522
+ if (r.error) {
523
+ desc = `Error injection \u2014 <code>${r.error}</code>`;
524
+ } else if (r.handler) {
490
525
  const found = handlers.has(r.handler);
491
526
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
527
+ } else if (r.scenarios?.length) {
528
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
529
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
492
530
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
493
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
531
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
494
532
  } else {
495
533
  const status = r.response?.status ?? 200;
496
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
534
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
497
535
  }
536
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
498
537
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
499
538
  }).join("")}
500
539
  </tbody></table>
@@ -646,7 +685,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
646
685
 
647
686
  <div class="banner">
648
687
  <div class="banner-inner">
649
- <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>`}
650
689
  <p>Zero-config REST API mock server</p>
651
690
  <div class="banner-meta">
652
691
  <span>URL <strong>${host}</strong></span>
@@ -698,7 +737,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
698
737
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
699
738
 
700
739
  <footer>
701
- Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
740
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
702
741
  </footer>
703
742
 
704
743
  </div>
@@ -733,6 +772,10 @@ function nextId(items) {
733
772
  const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
734
773
  return ids.length > 0 ? Math.max(...ids) + 1 : 1;
735
774
  }
775
+ function generateId(items, strategy) {
776
+ if (strategy === "uuid") return crypto.randomUUID();
777
+ return nextId(items);
778
+ }
736
779
  function firstParam(value) {
737
780
  if (value === void 0) return void 0;
738
781
  return Array.isArray(value) ? value[0] : value;
@@ -822,10 +865,10 @@ function findById(items, id) {
822
865
  function findIndexById(items, id) {
823
866
  return items.findIndex((i) => String(i["id"]) === id);
824
867
  }
825
- function createItem(storage, resource, body) {
868
+ function createItem(storage, resource, body, idStrategy = "increment") {
826
869
  const collection = storage.getCollection(resource) ?? [];
827
870
  const item = {
828
- id: body["id"] !== void 0 ? body["id"] : nextId(collection),
871
+ id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
829
872
  ...body
830
873
  };
831
874
  storage.setCollection(resource, [...collection, item]);
@@ -1004,7 +1047,12 @@ var CollectionRouteCommand = class {
1004
1047
  if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
1005
1048
  return reply.status(400).send({ error: "Request body must be a JSON object" });
1006
1049
  }
1007
- 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
+ );
1008
1056
  return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
1009
1057
  });
1010
1058
  }
@@ -1012,6 +1060,65 @@ var CollectionRouteCommand = class {
1012
1060
 
1013
1061
  // src/router/routes/custom.routes.ts
1014
1062
  init_cjs_shims();
1063
+
1064
+ // src/utils/conditions.ts
1065
+ init_cjs_shims();
1066
+ function resolveRequestPath(dotPath, req) {
1067
+ const [root, ...rest] = dotPath.split(".");
1068
+ let value;
1069
+ switch (root) {
1070
+ case "body":
1071
+ value = req.body;
1072
+ break;
1073
+ case "params":
1074
+ value = req.params;
1075
+ break;
1076
+ case "query":
1077
+ value = req.query;
1078
+ break;
1079
+ case "headers":
1080
+ value = req.headers;
1081
+ break;
1082
+ default:
1083
+ return void 0;
1084
+ }
1085
+ for (const key of rest) {
1086
+ if (value != null && typeof value === "object") {
1087
+ value = value[key];
1088
+ } else {
1089
+ return void 0;
1090
+ }
1091
+ }
1092
+ return value;
1093
+ }
1094
+ function matchConditionGroup(group, req) {
1095
+ return Object.entries(group).every(([key, expected]) => {
1096
+ const op = OPERATORS.find((o) => key.endsWith(o));
1097
+ if (op) {
1098
+ const path = key.slice(0, -op.length);
1099
+ const value2 = resolveRequestPath(path, req);
1100
+ if (value2 === void 0) return false;
1101
+ return applyOperator(value2, op, String(expected));
1102
+ }
1103
+ const value = resolveRequestPath(key, req);
1104
+ return String(value) === String(expected);
1105
+ });
1106
+ }
1107
+ function matchWhen(when, req) {
1108
+ if (Array.isArray(when)) {
1109
+ return when.some((group) => matchConditionGroup(group, req));
1110
+ }
1111
+ return matchConditionGroup(when, req);
1112
+ }
1113
+ function findMatchingScenario(scenarios, req) {
1114
+ return scenarios.find((s) => matchWhen(s.when, req));
1115
+ }
1116
+
1117
+ // src/router/routes/custom.routes.ts
1118
+ function resolveBody(body, ctx) {
1119
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1120
+ return body ?? null;
1121
+ }
1015
1122
  var CustomRouteCommand = class {
1016
1123
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
1017
1124
  this.storage = storage;
@@ -1036,6 +1143,13 @@ var CustomRouteCommand = class {
1036
1143
  method,
1037
1144
  url,
1038
1145
  handler: async (req, reply) => {
1146
+ if (route.delay && route.delay > 0) {
1147
+ await new Promise((resolve3) => setTimeout(resolve3, route.delay));
1148
+ }
1149
+ if (route.error) {
1150
+ const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
1151
+ return reply.status(route.error).send(body2);
1152
+ }
1039
1153
  for (const [key, value] of Object.entries(headers)) {
1040
1154
  reply.header(key, value);
1041
1155
  }
@@ -1045,13 +1159,13 @@ var CustomRouteCommand = class {
1045
1159
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1046
1160
  }
1047
1161
  try {
1048
- const ctx = {
1162
+ const ctx2 = {
1049
1163
  params: req.params,
1050
1164
  query: req.query,
1051
1165
  body: req.body,
1052
1166
  headers: req.headers
1053
1167
  };
1054
- const result = await fn(ctx);
1168
+ const result = await fn(ctx2);
1055
1169
  const resStatus = result.status ?? 200;
1056
1170
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1057
1171
  reply.header(k, v);
@@ -1063,12 +1177,24 @@ var CustomRouteCommand = class {
1063
1177
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1064
1178
  }
1065
1179
  }
1066
- const body = dynamic ? interpolate(rawBody, {
1180
+ const ctx = {
1067
1181
  params: req.params,
1068
1182
  query: req.query,
1069
1183
  body: req.body,
1070
1184
  headers: req.headers
1071
- }) : rawBody;
1185
+ };
1186
+ if (route.scenarios?.length) {
1187
+ const matched = findMatchingScenario(route.scenarios, ctx);
1188
+ const active = matched?.response ?? route.otherwise;
1189
+ if (active) {
1190
+ const aStatus = active.status ?? 200;
1191
+ const aBody = resolveBody(active.body, ctx);
1192
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1193
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1194
+ return reply.status(aStatus).send(aBody);
1195
+ }
1196
+ }
1197
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1072
1198
  if (body === null && status === 204) return reply.status(status).send();
1073
1199
  return reply.status(status).send(body);
1074
1200
  }
@@ -1294,7 +1420,7 @@ function createYrestServer(options) {
1294
1420
  return {
1295
1421
  async start() {
1296
1422
  const storage = "data" in options && options.data !== void 0 ? createInMemoryStorage(options.data) : (await Promise.resolve().then(() => (init_yrestStorage(), yrestStorage_exports))).createYrestStorage(resolvedOptions.file);
1297
- 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();
1298
1424
  _inner = createYrestServerFromStorage(storage, resolvedOptions, handlers);
1299
1425
  await _inner.start();
1300
1426
  },
@@ -1321,7 +1447,8 @@ function buildOptions(opts) {
1321
1447
  readonly: opts.readonly ?? false,
1322
1448
  delay: opts.delay ?? 0,
1323
1449
  handlers: opts.handlers,
1324
- 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"
1325
1452
  };
1326
1453
  }
1327
1454
  function createInMemoryStorage(data) {
@@ -1405,7 +1532,14 @@ var yrestOptionsSchema = import_zod.z.object({
1405
1532
  pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
1406
1533
  enabled: v !== false,
1407
1534
  limit: v === false || v === true ? 10 : v
1408
- }))
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")
1409
1543
  });
1410
1544
  // Annotate the CommonJS export names for ESM import in node:
1411
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)) {
@@ -454,16 +468,38 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
454
468
  <table><tbody>
455
469
  ${customRoutes.map((r) => {
456
470
  const fullPath = `${base}${r.path}`;
471
+ const tags = [];
472
+ if (r.error) {
473
+ tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
474
+ }
475
+ if (r.delay && r.delay > 0) {
476
+ tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
477
+ }
478
+ if (r.scenarios?.length) {
479
+ const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
480
+ tags.push(
481
+ `<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
482
+ );
483
+ }
484
+ if (r.otherwise) {
485
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
486
+ }
457
487
  let desc;
458
- if (r.handler) {
488
+ if (r.error) {
489
+ desc = `Error injection \u2014 <code>${r.error}</code>`;
490
+ } else if (r.handler) {
459
491
  const found = handlers.has(r.handler);
460
492
  desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
493
+ } else if (r.scenarios?.length) {
494
+ const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
495
+ desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
461
496
  } else if (r.response?.body != null && hasTemplates(r.response.body)) {
462
- desc = `Dynamic body \u2014 <code>{{\u2026}}</code>`;
497
+ desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
463
498
  } else {
464
499
  const status = r.response?.status ?? 200;
465
- desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + custom headers` : ""}`;
500
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
466
501
  }
502
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
467
503
  return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
468
504
  }).join("")}
469
505
  </tbody></table>
@@ -615,7 +651,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
615
651
 
616
652
  <div class="banner">
617
653
  <div class="banner-inner">
618
- <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>`}
619
655
  <p>Zero-config REST API mock server</p>
620
656
  <div class="banner-meta">
621
657
  <span>URL <strong>${host}</strong></span>
@@ -667,7 +703,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
667
703
  ${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
668
704
 
669
705
  <footer>
670
- Powered by <a href="https://github.com/aggiovato/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
706
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
671
707
  </footer>
672
708
 
673
709
  </div>
@@ -702,6 +738,10 @@ function nextId(items) {
702
738
  const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
703
739
  return ids.length > 0 ? Math.max(...ids) + 1 : 1;
704
740
  }
741
+ function generateId(items, strategy) {
742
+ if (strategy === "uuid") return crypto.randomUUID();
743
+ return nextId(items);
744
+ }
705
745
  function firstParam(value) {
706
746
  if (value === void 0) return void 0;
707
747
  return Array.isArray(value) ? value[0] : value;
@@ -791,10 +831,10 @@ function findById(items, id) {
791
831
  function findIndexById(items, id) {
792
832
  return items.findIndex((i) => String(i["id"]) === id);
793
833
  }
794
- function createItem(storage, resource, body) {
834
+ function createItem(storage, resource, body, idStrategy = "increment") {
795
835
  const collection = storage.getCollection(resource) ?? [];
796
836
  const item = {
797
- id: body["id"] !== void 0 ? body["id"] : nextId(collection),
837
+ id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
798
838
  ...body
799
839
  };
800
840
  storage.setCollection(resource, [...collection, item]);
@@ -973,7 +1013,12 @@ var CollectionRouteCommand = class {
973
1013
  if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
974
1014
  return reply.status(400).send({ error: "Request body must be a JSON object" });
975
1015
  }
976
- 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
+ );
977
1022
  return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
978
1023
  });
979
1024
  }
@@ -981,6 +1026,65 @@ var CollectionRouteCommand = class {
981
1026
 
982
1027
  // src/router/routes/custom.routes.ts
983
1028
  init_esm_shims();
1029
+
1030
+ // src/utils/conditions.ts
1031
+ init_esm_shims();
1032
+ function resolveRequestPath(dotPath, req) {
1033
+ const [root, ...rest] = dotPath.split(".");
1034
+ let value;
1035
+ switch (root) {
1036
+ case "body":
1037
+ value = req.body;
1038
+ break;
1039
+ case "params":
1040
+ value = req.params;
1041
+ break;
1042
+ case "query":
1043
+ value = req.query;
1044
+ break;
1045
+ case "headers":
1046
+ value = req.headers;
1047
+ break;
1048
+ default:
1049
+ return void 0;
1050
+ }
1051
+ for (const key of rest) {
1052
+ if (value != null && typeof value === "object") {
1053
+ value = value[key];
1054
+ } else {
1055
+ return void 0;
1056
+ }
1057
+ }
1058
+ return value;
1059
+ }
1060
+ function matchConditionGroup(group, req) {
1061
+ return Object.entries(group).every(([key, expected]) => {
1062
+ const op = OPERATORS.find((o) => key.endsWith(o));
1063
+ if (op) {
1064
+ const path2 = key.slice(0, -op.length);
1065
+ const value2 = resolveRequestPath(path2, req);
1066
+ if (value2 === void 0) return false;
1067
+ return applyOperator(value2, op, String(expected));
1068
+ }
1069
+ const value = resolveRequestPath(key, req);
1070
+ return String(value) === String(expected);
1071
+ });
1072
+ }
1073
+ function matchWhen(when, req) {
1074
+ if (Array.isArray(when)) {
1075
+ return when.some((group) => matchConditionGroup(group, req));
1076
+ }
1077
+ return matchConditionGroup(when, req);
1078
+ }
1079
+ function findMatchingScenario(scenarios, req) {
1080
+ return scenarios.find((s) => matchWhen(s.when, req));
1081
+ }
1082
+
1083
+ // src/router/routes/custom.routes.ts
1084
+ function resolveBody(body, ctx) {
1085
+ if (body != null && hasTemplates(body)) return interpolate(body, ctx);
1086
+ return body ?? null;
1087
+ }
984
1088
  var CustomRouteCommand = class {
985
1089
  constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
986
1090
  this.storage = storage;
@@ -1005,6 +1109,13 @@ var CustomRouteCommand = class {
1005
1109
  method,
1006
1110
  url,
1007
1111
  handler: async (req, reply) => {
1112
+ if (route.delay && route.delay > 0) {
1113
+ await new Promise((resolve3) => setTimeout(resolve3, route.delay));
1114
+ }
1115
+ if (route.error) {
1116
+ const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
1117
+ return reply.status(route.error).send(body2);
1118
+ }
1008
1119
  for (const [key, value] of Object.entries(headers)) {
1009
1120
  reply.header(key, value);
1010
1121
  }
@@ -1014,13 +1125,13 @@ var CustomRouteCommand = class {
1014
1125
  return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
1015
1126
  }
1016
1127
  try {
1017
- const ctx = {
1128
+ const ctx2 = {
1018
1129
  params: req.params,
1019
1130
  query: req.query,
1020
1131
  body: req.body,
1021
1132
  headers: req.headers
1022
1133
  };
1023
- const result = await fn(ctx);
1134
+ const result = await fn(ctx2);
1024
1135
  const resStatus = result.status ?? 200;
1025
1136
  for (const [k, v] of Object.entries(result.headers ?? {})) {
1026
1137
  reply.header(k, v);
@@ -1032,12 +1143,24 @@ var CustomRouteCommand = class {
1032
1143
  return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
1033
1144
  }
1034
1145
  }
1035
- const body = dynamic ? interpolate(rawBody, {
1146
+ const ctx = {
1036
1147
  params: req.params,
1037
1148
  query: req.query,
1038
1149
  body: req.body,
1039
1150
  headers: req.headers
1040
- }) : rawBody;
1151
+ };
1152
+ if (route.scenarios?.length) {
1153
+ const matched = findMatchingScenario(route.scenarios, ctx);
1154
+ const active = matched?.response ?? route.otherwise;
1155
+ if (active) {
1156
+ const aStatus = active.status ?? 200;
1157
+ const aBody = resolveBody(active.body, ctx);
1158
+ for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
1159
+ if (!active.body && aStatus === 204) return reply.status(aStatus).send();
1160
+ return reply.status(aStatus).send(aBody);
1161
+ }
1162
+ }
1163
+ const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
1041
1164
  if (body === null && status === 204) return reply.status(status).send();
1042
1165
  return reply.status(status).send(body);
1043
1166
  }
@@ -1290,7 +1413,8 @@ function buildOptions(opts) {
1290
1413
  readonly: opts.readonly ?? false,
1291
1414
  delay: opts.delay ?? 0,
1292
1415
  handlers: opts.handlers,
1293
- 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"
1294
1418
  };
1295
1419
  }
1296
1420
  function createInMemoryStorage(data) {
@@ -1374,7 +1498,14 @@ var yrestOptionsSchema = z.object({
1374
1498
  pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
1375
1499
  enabled: v !== false,
1376
1500
  limit: v === false || v === true ? 10 : v
1377
- }))
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")
1378
1509
  });
1379
1510
  export {
1380
1511
  createServer,