@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/README.md +40 -1
- package/assets/logo-color.png +0 -0
- package/assets/logo-figure.png +0 -0
- package/assets/logo-text.png +0 -0
- package/assets/logo-white.png +0 -0
- package/assets/yRest-banner.png +0 -0
- package/dist/cli/index.js +98 -41
- package/dist/cli/index.mjs +77 -20
- package/dist/index.d.mts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +78 -23
- package/dist/index.mjs +69 -17
- package/package.json +12 -4
package/dist/index.mjs
CHANGED
|
@@ -35,13 +35,13 @@ var yrestStorage_exports = {};
|
|
|
35
35
|
__export(yrestStorage_exports, {
|
|
36
36
|
createYrestStorage: () => createYrestStorage
|
|
37
37
|
});
|
|
38
|
-
import { readFileSync, writeFileSync, renameSync } from "fs";
|
|
39
|
-
import { resolve, dirname } from "path";
|
|
38
|
+
import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
39
|
+
import { resolve, dirname as dirname2 } from "path";
|
|
40
40
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
41
41
|
import { parse as parse2, stringify } from "yaml";
|
|
42
42
|
function createYrestStorage(filePath) {
|
|
43
43
|
const absPath = resolve(filePath);
|
|
44
|
-
const raw = parse2(
|
|
44
|
+
const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
|
|
45
45
|
const relations = raw["_rel"] ?? {};
|
|
46
46
|
const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
|
|
47
47
|
const data = Object.fromEntries(
|
|
@@ -73,12 +73,12 @@ function createYrestStorage(filePath) {
|
|
|
73
73
|
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
74
74
|
if (routes.length > 0) payload._routes = routes;
|
|
75
75
|
Object.assign(payload, data);
|
|
76
|
-
const tmp = resolve(
|
|
76
|
+
const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
|
|
77
77
|
writeFileSync(tmp, stringify(payload), "utf8");
|
|
78
78
|
renameSync(tmp, absPath);
|
|
79
79
|
},
|
|
80
80
|
reload() {
|
|
81
|
-
const fresh = parse2(
|
|
81
|
+
const fresh = parse2(readFileSync2(absPath, "utf8")) ?? {};
|
|
82
82
|
const freshRelations = fresh["_rel"] ?? {};
|
|
83
83
|
const freshData = Object.fromEntries(
|
|
84
84
|
Object.entries(fresh).filter(([key]) => key !== "_rel" && key !== "_routes")
|
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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"] :
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
}
|