@yrest/cli 0.7.0 → 0.8.1

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.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")
@@ -168,8 +168,15 @@ import { resolve as resolve2 } from "path";
168
168
  // src/utils/handlers.ts
169
169
  init_esm_shims();
170
170
  import { existsSync } from "fs";
171
+ var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
171
172
  async function loadHandlers(filePath) {
172
173
  if (!existsSync(filePath)) return /* @__PURE__ */ new Map();
174
+ if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
175
+ console.error(
176
+ ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
177
+ );
178
+ return /* @__PURE__ */ new Map();
179
+ }
173
180
  try {
174
181
  const mod = await import(filePath);
175
182
  const map = /* @__PURE__ */ new Map();
@@ -206,6 +213,9 @@ init_esm_shims();
206
213
 
207
214
  // src/router/templates/about.template.ts
208
215
  init_esm_shims();
216
+ import { readFileSync } from "fs";
217
+ import { dirname, join } from "path";
218
+ import { fileURLToPath as fileURLToPath2 } from "url";
209
219
 
210
220
  // src/utils/interpolate.ts
211
221
  init_esm_shims();
@@ -255,6 +265,15 @@ function hasTemplates(value) {
255
265
  }
256
266
 
257
267
  // src/router/templates/about.template.ts
268
+ var _dir = dirname(fileURLToPath2(import.meta.url));
269
+ var LOGO_SRC = (() => {
270
+ try {
271
+ const buf = readFileSync(join(_dir, "../../assets/logo-color.png"));
272
+ return `data:image/png;base64,${buf.toString("base64")}`;
273
+ } catch {
274
+ return "";
275
+ }
276
+ })();
258
277
  var METHOD_COLOR = {
259
278
  GET: "#3fb950",
260
279
  POST: "#58a6ff",
@@ -263,8 +282,11 @@ var METHOD_COLOR = {
263
282
  DELETE: "#f85149",
264
283
  fn: "#f0883e"
265
284
  };
285
+ function escapeHtml(str) {
286
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
287
+ }
266
288
  function badge(label, color, bg) {
267
- return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${label}</span>`;
289
+ return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${escapeHtml(label)}</span>`;
268
290
  }
269
291
  function methodBadge(method) {
270
292
  const color = METHOD_COLOR[method] ?? "#7d8590";
@@ -274,7 +296,7 @@ function endpointRow(method, path2, desc) {
274
296
  return `
275
297
  <tr>
276
298
  <td class="method-cell">${methodBadge(method)}</td>
277
- <td class="path-cell"><code>${path2}</code></td>
299
+ <td class="path-cell"><code>${escapeHtml(path2)}</code></td>
278
300
  <td class="desc-cell">${desc}</td>
279
301
  </tr>`;
280
302
  }
@@ -308,7 +330,7 @@ function resourceAccordion(name, base, isOpen) {
308
330
  return `
309
331
  <details class="resource-card" ${isOpen ? "open" : ""}>
310
332
  <summary>
311
- <span class="resource-name">/${name}</span>
333
+ <span class="resource-name">/${escapeHtml(name)}</span>
312
334
  <span class="route-count">6 routes</span>
313
335
  </summary>
314
336
  <table>
@@ -392,7 +414,7 @@ curl -X POST ${host}/_snapshot/reset`
392
414
  }
393
415
  if (firstCustomRoute) {
394
416
  const method = firstCustomRoute.method?.toUpperCase() ?? "GET";
395
- const fullPath = `${host}${base}${firstCustomRoute.path}`;
417
+ const fullPath = `${host}${base}${escapeHtml(firstCustomRoute.path ?? "")}`;
396
418
  const curlFlag = method === "GET" ? "" : `-X ${method} `;
397
419
  examples.push(`# Custom route
398
420
  curl ${curlFlag}${fullPath}`);
@@ -413,6 +435,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
413
435
  modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
414
436
  if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
415
437
  if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
438
+ if (options.idStrategy !== "increment")
439
+ modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
416
440
  const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
417
441
  const nestedRows = [];
418
442
  for (const [child, fields] of Object.entries(relations)) {
@@ -455,6 +479,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
455
479
  ${customRoutes.map((r) => {
456
480
  const fullPath = `${base}${r.path}`;
457
481
  const tags = [];
482
+ if (r.error) {
483
+ tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
484
+ }
458
485
  if (r.delay && r.delay > 0) {
459
486
  tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
460
487
  }
@@ -468,9 +495,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
468
495
  tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
469
496
  }
470
497
  let desc;
471
- if (r.handler) {
498
+ if (r.error) {
499
+ desc = `Error injection \u2014 <code>${r.error}</code>`;
500
+ } else if (r.handler) {
472
501
  const found = handlers.has(r.handler);
473
- desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
502
+ const handlerName = escapeHtml(r.handler);
503
+ desc = found ? `Handler \u2014 <code>${handlerName}()</code>` : `Handler \u2014 <code>${handlerName}()</code> <span style="color:#f85149">(not loaded)</span>`;
474
504
  } else if (r.scenarios?.length) {
475
505
  const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
476
506
  desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
@@ -632,7 +662,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
632
662
 
633
663
  <div class="banner">
634
664
  <div class="banner-inner">
635
- <h1><span class="y">y</span><span class="rest">Rest</span></h1>
665
+ ${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
666
  <p>Zero-config REST API mock server</p>
637
667
  <div class="banner-meta">
638
668
  <span>URL <strong>${host}</strong></span>
@@ -719,6 +749,10 @@ function nextId(items) {
719
749
  const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
720
750
  return ids.length > 0 ? Math.max(...ids) + 1 : 1;
721
751
  }
752
+ function generateId(items, strategy) {
753
+ if (strategy === "uuid") return crypto.randomUUID();
754
+ return nextId(items);
755
+ }
722
756
  function firstParam(value) {
723
757
  if (value === void 0) return void 0;
724
758
  return Array.isArray(value) ? value[0] : value;
@@ -744,6 +778,7 @@ function applyOperator(itemValue, op, filterValue) {
744
778
  case "_start":
745
779
  return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
746
780
  case "_regex": {
781
+ if (filterValue.length > 200) return false;
747
782
  try {
748
783
  return new RegExp(filterValue, "i").test(strItem);
749
784
  } catch {
@@ -808,10 +843,10 @@ function findById(items, id) {
808
843
  function findIndexById(items, id) {
809
844
  return items.findIndex((i) => String(i["id"]) === id);
810
845
  }
811
- function createItem(storage, resource, body) {
846
+ function createItem(storage, resource, body, idStrategy = "increment") {
812
847
  const collection = storage.getCollection(resource) ?? [];
813
848
  const item = {
814
- id: body["id"] !== void 0 ? body["id"] : nextId(collection),
849
+ id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
815
850
  ...body
816
851
  };
817
852
  storage.setCollection(resource, [...collection, item]);
@@ -990,7 +1025,12 @@ var CollectionRouteCommand = class {
990
1025
  if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
991
1026
  return reply.status(400).send({ error: "Request body must be a JSON object" });
992
1027
  }
993
- const item = createItem(this.storage, this.resource, req.body);
1028
+ const item = createItem(
1029
+ this.storage,
1030
+ this.resource,
1031
+ req.body,
1032
+ this.options.idStrategy
1033
+ );
994
1034
  return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
995
1035
  });
996
1036
  }
@@ -1084,6 +1124,10 @@ var CustomRouteCommand = class {
1084
1124
  if (route.delay && route.delay > 0) {
1085
1125
  await new Promise((resolve3) => setTimeout(resolve3, route.delay));
1086
1126
  }
1127
+ if (route.error) {
1128
+ const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
1129
+ return reply.status(route.error).send(body2);
1130
+ }
1087
1131
  for (const [key, value] of Object.entries(headers)) {
1088
1132
  reply.header(key, value);
1089
1133
  }
@@ -1381,7 +1425,8 @@ function buildOptions(opts) {
1381
1425
  readonly: opts.readonly ?? false,
1382
1426
  delay: opts.delay ?? 0,
1383
1427
  handlers: opts.handlers,
1384
- pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
1428
+ pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
1429
+ idStrategy: "increment"
1385
1430
  };
1386
1431
  }
1387
1432
  function createInMemoryStorage(data) {
@@ -1465,7 +1510,14 @@ var yrestOptionsSchema = z.object({
1465
1510
  pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
1466
1511
  enabled: v !== false,
1467
1512
  limit: v === false || v === true ? 10 : v
1468
- }))
1513
+ })),
1514
+ /**
1515
+ * Strategy used to generate `id` values for new items when no `id` is provided in the body.
1516
+ *
1517
+ * - `"increment"` (default) — next integer above the current max id in the collection.
1518
+ * - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
1519
+ */
1520
+ idStrategy: z.enum(["increment", "uuid"]).default("increment")
1469
1521
  });
1470
1522
  export {
1471
1523
  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.1",
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",
@@ -75,7 +77,7 @@
75
77
  },
76
78
  "devDependencies": {
77
79
  "@eslint/js": "^10.0.1",
78
- "@types/node": "^22.19.20",
80
+ "@types/node": "^22.19.21",
79
81
  "eslint": "^10.4.1",
80
82
  "eslint-config-prettier": "^10.1.8",
81
83
  "prettier": "^3.8.3",
@@ -86,5 +88,11 @@
86
88
  },
87
89
  "engines": {
88
90
  "node": ">=20"
91
+ },
92
+ "socket": {
93
+ "allow": {
94
+ "filesystem": true,
95
+ "dynamic-require": true
96
+ }
89
97
  }
90
98
  }