@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/cli/index.mjs
CHANGED
|
@@ -106,7 +106,7 @@ function registerInit(program2) {
|
|
|
106
106
|
|
|
107
107
|
// src/cli/commands/serve.ts
|
|
108
108
|
import { watchFile } from "fs";
|
|
109
|
-
import { join, resolve as resolve3 } from "path";
|
|
109
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
110
110
|
|
|
111
111
|
// src/storage/yrestStorage.ts
|
|
112
112
|
import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
@@ -196,6 +196,11 @@ function createYrestStorage(filePath) {
|
|
|
196
196
|
import Fastify from "fastify";
|
|
197
197
|
import cors from "@fastify/cors";
|
|
198
198
|
|
|
199
|
+
// src/router/templates/about.template.ts
|
|
200
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
201
|
+
import { dirname as dirname2, join } from "path";
|
|
202
|
+
import { fileURLToPath } from "url";
|
|
203
|
+
|
|
199
204
|
// src/utils/interpolate.ts
|
|
200
205
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
201
206
|
function getPath(obj, path) {
|
|
@@ -243,6 +248,15 @@ function hasTemplates(value) {
|
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
// src/router/templates/about.template.ts
|
|
251
|
+
var _dir = dirname2(fileURLToPath(import.meta.url));
|
|
252
|
+
var LOGO_SRC = (() => {
|
|
253
|
+
try {
|
|
254
|
+
const buf = readFileSync2(join(_dir, "../../assets/logo-color.png"));
|
|
255
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
256
|
+
} catch {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
246
260
|
var METHOD_COLOR = {
|
|
247
261
|
GET: "#3fb950",
|
|
248
262
|
POST: "#58a6ff",
|
|
@@ -251,8 +265,11 @@ var METHOD_COLOR = {
|
|
|
251
265
|
DELETE: "#f85149",
|
|
252
266
|
fn: "#f0883e"
|
|
253
267
|
};
|
|
268
|
+
function escapeHtml(str) {
|
|
269
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
270
|
+
}
|
|
254
271
|
function badge(label, color, bg) {
|
|
255
|
-
return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${label}</span>`;
|
|
272
|
+
return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${escapeHtml(label)}</span>`;
|
|
256
273
|
}
|
|
257
274
|
function methodBadge(method) {
|
|
258
275
|
const color = METHOD_COLOR[method] ?? "#7d8590";
|
|
@@ -262,7 +279,7 @@ function endpointRow(method, path, desc) {
|
|
|
262
279
|
return `
|
|
263
280
|
<tr>
|
|
264
281
|
<td class="method-cell">${methodBadge(method)}</td>
|
|
265
|
-
<td class="path-cell"><code>${path}</code></td>
|
|
282
|
+
<td class="path-cell"><code>${escapeHtml(path)}</code></td>
|
|
266
283
|
<td class="desc-cell">${desc}</td>
|
|
267
284
|
</tr>`;
|
|
268
285
|
}
|
|
@@ -296,7 +313,7 @@ function resourceAccordion(name, base, isOpen) {
|
|
|
296
313
|
return `
|
|
297
314
|
<details class="resource-card" ${isOpen ? "open" : ""}>
|
|
298
315
|
<summary>
|
|
299
|
-
<span class="resource-name">/${name}</span>
|
|
316
|
+
<span class="resource-name">/${escapeHtml(name)}</span>
|
|
300
317
|
<span class="route-count">6 routes</span>
|
|
301
318
|
</summary>
|
|
302
319
|
<table>
|
|
@@ -380,7 +397,7 @@ curl -X POST ${host}/_snapshot/reset`
|
|
|
380
397
|
}
|
|
381
398
|
if (firstCustomRoute) {
|
|
382
399
|
const method = firstCustomRoute.method?.toUpperCase() ?? "GET";
|
|
383
|
-
const fullPath = `${host}${base}${firstCustomRoute.path}`;
|
|
400
|
+
const fullPath = `${host}${base}${escapeHtml(firstCustomRoute.path ?? "")}`;
|
|
384
401
|
const curlFlag = method === "GET" ? "" : `-X ${method} `;
|
|
385
402
|
examples.push(`# Custom route
|
|
386
403
|
curl ${curlFlag}${fullPath}`);
|
|
@@ -401,6 +418,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
401
418
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
402
419
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
403
420
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
421
|
+
if (options.idStrategy !== "increment")
|
|
422
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
404
423
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
405
424
|
const nestedRows = [];
|
|
406
425
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -443,6 +462,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
443
462
|
${customRoutes.map((r) => {
|
|
444
463
|
const fullPath = `${base}${r.path}`;
|
|
445
464
|
const tags = [];
|
|
465
|
+
if (r.error) {
|
|
466
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
467
|
+
}
|
|
446
468
|
if (r.delay && r.delay > 0) {
|
|
447
469
|
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
448
470
|
}
|
|
@@ -456,9 +478,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
456
478
|
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
457
479
|
}
|
|
458
480
|
let desc;
|
|
459
|
-
if (r.
|
|
481
|
+
if (r.error) {
|
|
482
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
483
|
+
} else if (r.handler) {
|
|
460
484
|
const found = handlers.has(r.handler);
|
|
461
|
-
|
|
485
|
+
const handlerName = escapeHtml(r.handler);
|
|
486
|
+
desc = found ? `Handler \u2014 <code>${handlerName}()</code>` : `Handler \u2014 <code>${handlerName}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
462
487
|
} else if (r.scenarios?.length) {
|
|
463
488
|
const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
|
|
464
489
|
desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
|
|
@@ -620,7 +645,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
620
645
|
|
|
621
646
|
<div class="banner">
|
|
622
647
|
<div class="banner-inner">
|
|
623
|
-
|
|
648
|
+
${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
649
|
<p>Zero-config REST API mock server</p>
|
|
625
650
|
<div class="banner-meta">
|
|
626
651
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -703,6 +728,10 @@ function nextId(items) {
|
|
|
703
728
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
704
729
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
705
730
|
}
|
|
731
|
+
function generateId(items, strategy) {
|
|
732
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
733
|
+
return nextId(items);
|
|
734
|
+
}
|
|
706
735
|
function firstParam(value) {
|
|
707
736
|
if (value === void 0) return void 0;
|
|
708
737
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -727,6 +756,7 @@ function applyOperator(itemValue, op, filterValue) {
|
|
|
727
756
|
case "_start":
|
|
728
757
|
return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
|
|
729
758
|
case "_regex": {
|
|
759
|
+
if (filterValue.length > 200) return false;
|
|
730
760
|
try {
|
|
731
761
|
return new RegExp(filterValue, "i").test(strItem);
|
|
732
762
|
} catch {
|
|
@@ -790,10 +820,10 @@ function findById(items, id) {
|
|
|
790
820
|
function findIndexById(items, id) {
|
|
791
821
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
792
822
|
}
|
|
793
|
-
function createItem(storage, resource, body) {
|
|
823
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
794
824
|
const collection = storage.getCollection(resource) ?? [];
|
|
795
825
|
const item = {
|
|
796
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
826
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
797
827
|
...body
|
|
798
828
|
};
|
|
799
829
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -971,7 +1001,12 @@ var CollectionRouteCommand = class {
|
|
|
971
1001
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
972
1002
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
973
1003
|
}
|
|
974
|
-
const item = createItem(
|
|
1004
|
+
const item = createItem(
|
|
1005
|
+
this.storage,
|
|
1006
|
+
this.resource,
|
|
1007
|
+
req.body,
|
|
1008
|
+
this.options.idStrategy
|
|
1009
|
+
);
|
|
975
1010
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
976
1011
|
});
|
|
977
1012
|
}
|
|
@@ -1061,6 +1096,10 @@ var CustomRouteCommand = class {
|
|
|
1061
1096
|
if (route.delay && route.delay > 0) {
|
|
1062
1097
|
await new Promise((resolve5) => setTimeout(resolve5, route.delay));
|
|
1063
1098
|
}
|
|
1099
|
+
if (route.error) {
|
|
1100
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1101
|
+
return reply.status(route.error).send(body2);
|
|
1102
|
+
}
|
|
1064
1103
|
for (const [key, value] of Object.entries(headers)) {
|
|
1065
1104
|
reply.header(key, value);
|
|
1066
1105
|
}
|
|
@@ -1357,22 +1396,36 @@ var yrestOptionsSchema = z.object({
|
|
|
1357
1396
|
pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1358
1397
|
enabled: v !== false,
|
|
1359
1398
|
limit: v === false || v === true ? 10 : v
|
|
1360
|
-
}))
|
|
1399
|
+
})),
|
|
1400
|
+
/**
|
|
1401
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1402
|
+
*
|
|
1403
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1404
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1405
|
+
*/
|
|
1406
|
+
idStrategy: z.enum(["increment", "uuid"]).default("increment")
|
|
1361
1407
|
});
|
|
1362
1408
|
|
|
1363
1409
|
// src/config/loadConfigFile.ts
|
|
1364
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1410
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1365
1411
|
import { parse as parse2 } from "yaml";
|
|
1366
1412
|
function loadConfigFile(configPath) {
|
|
1367
1413
|
if (!existsSync2(configPath)) return {};
|
|
1368
|
-
const raw =
|
|
1414
|
+
const raw = readFileSync3(configPath, "utf8");
|
|
1369
1415
|
return parse2(raw) ?? {};
|
|
1370
1416
|
}
|
|
1371
1417
|
|
|
1372
1418
|
// src/utils/handlers.ts
|
|
1373
1419
|
import { existsSync as existsSync3 } from "fs";
|
|
1420
|
+
var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
|
|
1374
1421
|
async function loadHandlers(filePath) {
|
|
1375
1422
|
if (!existsSync3(filePath)) return /* @__PURE__ */ new Map();
|
|
1423
|
+
if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
|
|
1424
|
+
console.error(
|
|
1425
|
+
` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
|
|
1426
|
+
);
|
|
1427
|
+
return /* @__PURE__ */ new Map();
|
|
1428
|
+
}
|
|
1376
1429
|
try {
|
|
1377
1430
|
const mod = await import(filePath);
|
|
1378
1431
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1402,8 +1455,12 @@ function registerServe(program2) {
|
|
|
1402
1455
|
).option(
|
|
1403
1456
|
"--handlers <file>",
|
|
1404
1457
|
"Path to a JavaScript file exporting custom route handler functions"
|
|
1458
|
+
).option(
|
|
1459
|
+
"--id-strategy <strategy>",
|
|
1460
|
+
"Id generation strategy for new items: increment (default) or uuid",
|
|
1461
|
+
"increment"
|
|
1405
1462
|
).action(async (file, flags, cmd) => {
|
|
1406
|
-
const fileConfig = loadConfigFile(
|
|
1463
|
+
const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
|
|
1407
1464
|
const cliOverrides = Object.fromEntries(
|
|
1408
1465
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
1409
1466
|
);
|
|
@@ -1506,8 +1563,8 @@ function registerServe(program2) {
|
|
|
1506
1563
|
}
|
|
1507
1564
|
|
|
1508
1565
|
// src/cli/commands/handler.ts
|
|
1509
|
-
import { existsSync as existsSync4, readFileSync as
|
|
1510
|
-
import { join as
|
|
1566
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1567
|
+
import { join as join3, resolve as resolve4, basename } from "path";
|
|
1511
1568
|
import { parse as parse3, stringify as stringify2 } from "yaml";
|
|
1512
1569
|
var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
|
|
1513
1570
|
// Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
|
|
@@ -1534,7 +1591,7 @@ function registerHandler(program2) {
|
|
|
1534
1591
|
"--register",
|
|
1535
1592
|
"Also add a _routes entry to db.yml linking this handler to method + path"
|
|
1536
1593
|
).action((name, flags) => {
|
|
1537
|
-
const fileConfig = loadConfigFile(
|
|
1594
|
+
const fileConfig = loadConfigFile(join3(process.cwd(), "yrest.config.yml"));
|
|
1538
1595
|
const handlersPath = resolve4(
|
|
1539
1596
|
fileConfig.handlers ?? "yrest.handlers.js"
|
|
1540
1597
|
);
|
|
@@ -1547,7 +1604,7 @@ function registerHandler(program2) {
|
|
|
1547
1604
|
);
|
|
1548
1605
|
console.log(` Created ${basename(handlersPath)}`);
|
|
1549
1606
|
} else {
|
|
1550
|
-
const existing =
|
|
1607
|
+
const existing = readFileSync4(handlersPath, "utf8");
|
|
1551
1608
|
if (existing.includes(`function ${name}(`)) {
|
|
1552
1609
|
console.error(` Error: handler "${name}" already exists in ${basename(handlersPath)}`);
|
|
1553
1610
|
process.exit(1);
|
|
@@ -1564,7 +1621,7 @@ function registerHandler(program2) {
|
|
|
1564
1621
|
console.error(` Error: database file not found at ${dbPath}`);
|
|
1565
1622
|
process.exit(1);
|
|
1566
1623
|
}
|
|
1567
|
-
const raw = parse3(
|
|
1624
|
+
const raw = parse3(readFileSync4(dbPath, "utf8")) ?? {};
|
|
1568
1625
|
if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
|
|
1569
1626
|
const routes = raw["_routes"];
|
|
1570
1627
|
const alreadyRegistered = routes.some((r) => r["handler"] === name);
|
package/dist/index.d.mts
CHANGED
|
@@ -112,6 +112,19 @@ type CustomRoute = {
|
|
|
112
112
|
* Applied before any response is sent, regardless of which path resolved the response.
|
|
113
113
|
*/
|
|
114
114
|
delay?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Forces this route to always return the given HTTP status code as an error,
|
|
117
|
+
* bypassing handlers, scenarios and the static response entirely.
|
|
118
|
+
* Applied after `delay:` so slow-error scenarios still work.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Always return 503
|
|
122
|
+
* error: 503
|
|
123
|
+
* errorBody: { message: "Service unavailable" }
|
|
124
|
+
*/
|
|
125
|
+
error?: number;
|
|
126
|
+
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
127
|
+
errorBody?: unknown;
|
|
115
128
|
};
|
|
116
129
|
/**
|
|
117
130
|
* In-memory store backed by a YAML file.
|
|
@@ -251,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
251
264
|
enabled: boolean;
|
|
252
265
|
limit: number;
|
|
253
266
|
}, number | boolean | undefined>;
|
|
267
|
+
/**
|
|
268
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
269
|
+
*
|
|
270
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
271
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
272
|
+
*/
|
|
273
|
+
idStrategy: z.ZodDefault<z.ZodEnum<["increment", "uuid"]>>;
|
|
254
274
|
}, "strip", z.ZodTypeAny, {
|
|
255
275
|
file: string;
|
|
256
276
|
port: number;
|
|
@@ -264,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
264
284
|
enabled: boolean;
|
|
265
285
|
limit: number;
|
|
266
286
|
};
|
|
287
|
+
idStrategy: "increment" | "uuid";
|
|
267
288
|
handlers?: string | undefined;
|
|
268
289
|
}, {
|
|
269
290
|
file: string;
|
|
@@ -276,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
276
297
|
delay?: number | undefined;
|
|
277
298
|
handlers?: string | undefined;
|
|
278
299
|
pageable?: number | boolean | undefined;
|
|
300
|
+
idStrategy?: "increment" | "uuid" | undefined;
|
|
279
301
|
}>;
|
|
280
302
|
/**
|
|
281
303
|
* Resolved server configuration after Zod validation and transformation.
|
package/dist/index.d.ts
CHANGED
|
@@ -112,6 +112,19 @@ type CustomRoute = {
|
|
|
112
112
|
* Applied before any response is sent, regardless of which path resolved the response.
|
|
113
113
|
*/
|
|
114
114
|
delay?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Forces this route to always return the given HTTP status code as an error,
|
|
117
|
+
* bypassing handlers, scenarios and the static response entirely.
|
|
118
|
+
* Applied after `delay:` so slow-error scenarios still work.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Always return 503
|
|
122
|
+
* error: 503
|
|
123
|
+
* errorBody: { message: "Service unavailable" }
|
|
124
|
+
*/
|
|
125
|
+
error?: number;
|
|
126
|
+
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
127
|
+
errorBody?: unknown;
|
|
115
128
|
};
|
|
116
129
|
/**
|
|
117
130
|
* In-memory store backed by a YAML file.
|
|
@@ -251,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
251
264
|
enabled: boolean;
|
|
252
265
|
limit: number;
|
|
253
266
|
}, number | boolean | undefined>;
|
|
267
|
+
/**
|
|
268
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
269
|
+
*
|
|
270
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
271
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
272
|
+
*/
|
|
273
|
+
idStrategy: z.ZodDefault<z.ZodEnum<["increment", "uuid"]>>;
|
|
254
274
|
}, "strip", z.ZodTypeAny, {
|
|
255
275
|
file: string;
|
|
256
276
|
port: number;
|
|
@@ -264,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
264
284
|
enabled: boolean;
|
|
265
285
|
limit: number;
|
|
266
286
|
};
|
|
287
|
+
idStrategy: "increment" | "uuid";
|
|
267
288
|
handlers?: string | undefined;
|
|
268
289
|
}, {
|
|
269
290
|
file: string;
|
|
@@ -276,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
276
297
|
delay?: number | undefined;
|
|
277
298
|
handlers?: string | undefined;
|
|
278
299
|
pageable?: number | boolean | undefined;
|
|
300
|
+
idStrategy?: "increment" | "uuid" | undefined;
|
|
279
301
|
}>;
|
|
280
302
|
/**
|
|
281
303
|
* Resolved server configuration after Zod validation and transformation.
|
package/dist/index.js
CHANGED
|
@@ -31,9 +31,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var getImportMetaUrl, importMetaUrl;
|
|
34
35
|
var init_cjs_shims = __esm({
|
|
35
36
|
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
36
37
|
"use strict";
|
|
38
|
+
getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
39
|
+
importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
37
40
|
}
|
|
38
41
|
});
|
|
39
42
|
|
|
@@ -56,8 +59,8 @@ __export(yrestStorage_exports, {
|
|
|
56
59
|
createYrestStorage: () => createYrestStorage
|
|
57
60
|
});
|
|
58
61
|
function createYrestStorage(filePath) {
|
|
59
|
-
const absPath = (0,
|
|
60
|
-
const raw = (0, import_yaml2.parse)((0,
|
|
62
|
+
const absPath = (0, import_node_path2.resolve)(filePath);
|
|
63
|
+
const raw = (0, import_yaml2.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
61
64
|
const relations = raw["_rel"] ?? {};
|
|
62
65
|
const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
|
|
63
66
|
const data = Object.fromEntries(
|
|
@@ -89,12 +92,12 @@ function createYrestStorage(filePath) {
|
|
|
89
92
|
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
90
93
|
if (routes.length > 0) payload._routes = routes;
|
|
91
94
|
Object.assign(payload, data);
|
|
92
|
-
const tmp = (0,
|
|
93
|
-
(0,
|
|
94
|
-
(0,
|
|
95
|
+
const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
|
|
96
|
+
(0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml2.stringify)(payload), "utf8");
|
|
97
|
+
(0, import_node_fs3.renameSync)(tmp, absPath);
|
|
95
98
|
},
|
|
96
99
|
reload() {
|
|
97
|
-
const fresh = (0, import_yaml2.parse)((0,
|
|
100
|
+
const fresh = (0, import_yaml2.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
98
101
|
const freshRelations = fresh["_rel"] ?? {};
|
|
99
102
|
const freshData = Object.fromEntries(
|
|
100
103
|
Object.entries(fresh).filter(([key]) => key !== "_rel" && key !== "_routes")
|
|
@@ -124,13 +127,13 @@ function createYrestStorage(filePath) {
|
|
|
124
127
|
}
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
|
-
var
|
|
130
|
+
var import_node_fs3, import_node_path2, import_node_crypto2, import_yaml2;
|
|
128
131
|
var init_yrestStorage = __esm({
|
|
129
132
|
"src/storage/yrestStorage.ts"() {
|
|
130
133
|
"use strict";
|
|
131
134
|
init_cjs_shims();
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
import_node_fs3 = require("fs");
|
|
136
|
+
import_node_path2 = require("path");
|
|
134
137
|
import_node_crypto2 = require("crypto");
|
|
135
138
|
import_yaml2 = require("yaml");
|
|
136
139
|
init_deepCopy();
|
|
@@ -194,13 +197,20 @@ function dedent(str) {
|
|
|
194
197
|
|
|
195
198
|
// src/api/yrestServer.ts
|
|
196
199
|
init_cjs_shims();
|
|
197
|
-
var
|
|
200
|
+
var import_node_path3 = require("path");
|
|
198
201
|
|
|
199
202
|
// src/utils/handlers.ts
|
|
200
203
|
init_cjs_shims();
|
|
201
204
|
var import_node_fs = require("fs");
|
|
205
|
+
var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
|
|
202
206
|
async function loadHandlers(filePath) {
|
|
203
207
|
if (!(0, import_node_fs.existsSync)(filePath)) return /* @__PURE__ */ new Map();
|
|
208
|
+
if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
|
|
209
|
+
console.error(
|
|
210
|
+
` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
|
|
211
|
+
);
|
|
212
|
+
return /* @__PURE__ */ new Map();
|
|
213
|
+
}
|
|
204
214
|
try {
|
|
205
215
|
const mod = await import(filePath);
|
|
206
216
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -237,6 +247,9 @@ init_cjs_shims();
|
|
|
237
247
|
|
|
238
248
|
// src/router/templates/about.template.ts
|
|
239
249
|
init_cjs_shims();
|
|
250
|
+
var import_node_fs2 = require("fs");
|
|
251
|
+
var import_node_path = require("path");
|
|
252
|
+
var import_node_url = require("url");
|
|
240
253
|
|
|
241
254
|
// src/utils/interpolate.ts
|
|
242
255
|
init_cjs_shims();
|
|
@@ -286,6 +299,15 @@ function hasTemplates(value) {
|
|
|
286
299
|
}
|
|
287
300
|
|
|
288
301
|
// src/router/templates/about.template.ts
|
|
302
|
+
var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
303
|
+
var LOGO_SRC = (() => {
|
|
304
|
+
try {
|
|
305
|
+
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/logo-color.png"));
|
|
306
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
307
|
+
} catch {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
})();
|
|
289
311
|
var METHOD_COLOR = {
|
|
290
312
|
GET: "#3fb950",
|
|
291
313
|
POST: "#58a6ff",
|
|
@@ -294,8 +316,11 @@ var METHOD_COLOR = {
|
|
|
294
316
|
DELETE: "#f85149",
|
|
295
317
|
fn: "#f0883e"
|
|
296
318
|
};
|
|
319
|
+
function escapeHtml(str) {
|
|
320
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
321
|
+
}
|
|
297
322
|
function badge(label, color, bg) {
|
|
298
|
-
return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${label}</span>`;
|
|
323
|
+
return `<span class="badge" style="background:${bg};color:${color};border:1px solid ${color}40">${escapeHtml(label)}</span>`;
|
|
299
324
|
}
|
|
300
325
|
function methodBadge(method) {
|
|
301
326
|
const color = METHOD_COLOR[method] ?? "#7d8590";
|
|
@@ -305,7 +330,7 @@ function endpointRow(method, path, desc) {
|
|
|
305
330
|
return `
|
|
306
331
|
<tr>
|
|
307
332
|
<td class="method-cell">${methodBadge(method)}</td>
|
|
308
|
-
<td class="path-cell"><code>${path}</code></td>
|
|
333
|
+
<td class="path-cell"><code>${escapeHtml(path)}</code></td>
|
|
309
334
|
<td class="desc-cell">${desc}</td>
|
|
310
335
|
</tr>`;
|
|
311
336
|
}
|
|
@@ -339,7 +364,7 @@ function resourceAccordion(name, base, isOpen) {
|
|
|
339
364
|
return `
|
|
340
365
|
<details class="resource-card" ${isOpen ? "open" : ""}>
|
|
341
366
|
<summary>
|
|
342
|
-
<span class="resource-name">/${name}</span>
|
|
367
|
+
<span class="resource-name">/${escapeHtml(name)}</span>
|
|
343
368
|
<span class="route-count">6 routes</span>
|
|
344
369
|
</summary>
|
|
345
370
|
<table>
|
|
@@ -423,7 +448,7 @@ curl -X POST ${host}/_snapshot/reset`
|
|
|
423
448
|
}
|
|
424
449
|
if (firstCustomRoute) {
|
|
425
450
|
const method = firstCustomRoute.method?.toUpperCase() ?? "GET";
|
|
426
|
-
const fullPath = `${host}${base}${firstCustomRoute.path}`;
|
|
451
|
+
const fullPath = `${host}${base}${escapeHtml(firstCustomRoute.path ?? "")}`;
|
|
427
452
|
const curlFlag = method === "GET" ? "" : `-X ${method} `;
|
|
428
453
|
examples.push(`# Custom route
|
|
429
454
|
curl ${curlFlag}${fullPath}`);
|
|
@@ -444,6 +469,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
444
469
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
445
470
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
446
471
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
472
|
+
if (options.idStrategy !== "increment")
|
|
473
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
447
474
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
448
475
|
const nestedRows = [];
|
|
449
476
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -486,6 +513,9 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
486
513
|
${customRoutes.map((r) => {
|
|
487
514
|
const fullPath = `${base}${r.path}`;
|
|
488
515
|
const tags = [];
|
|
516
|
+
if (r.error) {
|
|
517
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
518
|
+
}
|
|
489
519
|
if (r.delay && r.delay > 0) {
|
|
490
520
|
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
491
521
|
}
|
|
@@ -499,9 +529,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
499
529
|
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
500
530
|
}
|
|
501
531
|
let desc;
|
|
502
|
-
if (r.
|
|
532
|
+
if (r.error) {
|
|
533
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
534
|
+
} else if (r.handler) {
|
|
503
535
|
const found = handlers.has(r.handler);
|
|
504
|
-
|
|
536
|
+
const handlerName = escapeHtml(r.handler);
|
|
537
|
+
desc = found ? `Handler \u2014 <code>${handlerName}()</code>` : `Handler \u2014 <code>${handlerName}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
505
538
|
} else if (r.scenarios?.length) {
|
|
506
539
|
const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
|
|
507
540
|
desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
|
|
@@ -663,7 +696,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
663
696
|
|
|
664
697
|
<div class="banner">
|
|
665
698
|
<div class="banner-inner">
|
|
666
|
-
|
|
699
|
+
${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
700
|
<p>Zero-config REST API mock server</p>
|
|
668
701
|
<div class="banner-meta">
|
|
669
702
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -750,6 +783,10 @@ function nextId(items) {
|
|
|
750
783
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
751
784
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
752
785
|
}
|
|
786
|
+
function generateId(items, strategy) {
|
|
787
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
788
|
+
return nextId(items);
|
|
789
|
+
}
|
|
753
790
|
function firstParam(value) {
|
|
754
791
|
if (value === void 0) return void 0;
|
|
755
792
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -775,6 +812,7 @@ function applyOperator(itemValue, op, filterValue) {
|
|
|
775
812
|
case "_start":
|
|
776
813
|
return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
|
|
777
814
|
case "_regex": {
|
|
815
|
+
if (filterValue.length > 200) return false;
|
|
778
816
|
try {
|
|
779
817
|
return new RegExp(filterValue, "i").test(strItem);
|
|
780
818
|
} catch {
|
|
@@ -839,10 +877,10 @@ function findById(items, id) {
|
|
|
839
877
|
function findIndexById(items, id) {
|
|
840
878
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
841
879
|
}
|
|
842
|
-
function createItem(storage, resource, body) {
|
|
880
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
843
881
|
const collection = storage.getCollection(resource) ?? [];
|
|
844
882
|
const item = {
|
|
845
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
883
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
846
884
|
...body
|
|
847
885
|
};
|
|
848
886
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -1021,7 +1059,12 @@ var CollectionRouteCommand = class {
|
|
|
1021
1059
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
1022
1060
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
1023
1061
|
}
|
|
1024
|
-
const item = createItem(
|
|
1062
|
+
const item = createItem(
|
|
1063
|
+
this.storage,
|
|
1064
|
+
this.resource,
|
|
1065
|
+
req.body,
|
|
1066
|
+
this.options.idStrategy
|
|
1067
|
+
);
|
|
1025
1068
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
1026
1069
|
});
|
|
1027
1070
|
}
|
|
@@ -1115,6 +1158,10 @@ var CustomRouteCommand = class {
|
|
|
1115
1158
|
if (route.delay && route.delay > 0) {
|
|
1116
1159
|
await new Promise((resolve3) => setTimeout(resolve3, route.delay));
|
|
1117
1160
|
}
|
|
1161
|
+
if (route.error) {
|
|
1162
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1163
|
+
return reply.status(route.error).send(body2);
|
|
1164
|
+
}
|
|
1118
1165
|
for (const [key, value] of Object.entries(headers)) {
|
|
1119
1166
|
reply.header(key, value);
|
|
1120
1167
|
}
|
|
@@ -1385,7 +1432,7 @@ function createYrestServer(options) {
|
|
|
1385
1432
|
return {
|
|
1386
1433
|
async start() {
|
|
1387
1434
|
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,
|
|
1435
|
+
const handlers = resolvedOptions.handlers ? await loadHandlers((0, import_node_path3.resolve)(resolvedOptions.handlers)) : /* @__PURE__ */ new Map();
|
|
1389
1436
|
_inner = createYrestServerFromStorage(storage, resolvedOptions, handlers);
|
|
1390
1437
|
await _inner.start();
|
|
1391
1438
|
},
|
|
@@ -1412,7 +1459,8 @@ function buildOptions(opts) {
|
|
|
1412
1459
|
readonly: opts.readonly ?? false,
|
|
1413
1460
|
delay: opts.delay ?? 0,
|
|
1414
1461
|
handlers: opts.handlers,
|
|
1415
|
-
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
|
|
1462
|
+
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
|
|
1463
|
+
idStrategy: "increment"
|
|
1416
1464
|
};
|
|
1417
1465
|
}
|
|
1418
1466
|
function createInMemoryStorage(data) {
|
|
@@ -1496,7 +1544,14 @@ var yrestOptionsSchema = import_zod.z.object({
|
|
|
1496
1544
|
pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1497
1545
|
enabled: v !== false,
|
|
1498
1546
|
limit: v === false || v === true ? 10 : v
|
|
1499
|
-
}))
|
|
1547
|
+
})),
|
|
1548
|
+
/**
|
|
1549
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1550
|
+
*
|
|
1551
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1552
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1553
|
+
*/
|
|
1554
|
+
idStrategy: import_zod.z.enum(["increment", "uuid"]).default("increment")
|
|
1500
1555
|
});
|
|
1501
1556
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1502
1557
|
0 && (module.exports = {
|