@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/README.md +156 -18
- 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 +176 -43
- package/dist/cli/index.mjs +155 -22
- package/dist/index.d.mts +95 -16
- package/dist/index.d.ts +95 -16
- package/dist/index.js +159 -25
- package/dist/index.mjs +150 -19
- package/package.json +10 -3
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",
|
|
@@ -401,6 +415,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
401
415
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
402
416
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
403
417
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
418
|
+
if (options.idStrategy !== "increment")
|
|
419
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
404
420
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
405
421
|
const nestedRows = [];
|
|
406
422
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -442,16 +458,38 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
442
458
|
<table><tbody>
|
|
443
459
|
${customRoutes.map((r) => {
|
|
444
460
|
const fullPath = `${base}${r.path}`;
|
|
461
|
+
const tags = [];
|
|
462
|
+
if (r.error) {
|
|
463
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
464
|
+
}
|
|
465
|
+
if (r.delay && r.delay > 0) {
|
|
466
|
+
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
467
|
+
}
|
|
468
|
+
if (r.scenarios?.length) {
|
|
469
|
+
const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
|
|
470
|
+
tags.push(
|
|
471
|
+
`<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (r.otherwise) {
|
|
475
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
476
|
+
}
|
|
445
477
|
let desc;
|
|
446
|
-
if (r.
|
|
478
|
+
if (r.error) {
|
|
479
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
480
|
+
} else if (r.handler) {
|
|
447
481
|
const found = handlers.has(r.handler);
|
|
448
482
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
483
|
+
} else if (r.scenarios?.length) {
|
|
484
|
+
const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
|
|
485
|
+
desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
|
|
449
486
|
} else if (r.response?.body != null && hasTemplates(r.response.body)) {
|
|
450
|
-
desc = `Dynamic
|
|
487
|
+
desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
|
|
451
488
|
} else {
|
|
452
489
|
const status = r.response?.status ?? 200;
|
|
453
|
-
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` +
|
|
490
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
454
491
|
}
|
|
492
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
455
493
|
return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
|
|
456
494
|
}).join("")}
|
|
457
495
|
</tbody></table>
|
|
@@ -603,7 +641,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
603
641
|
|
|
604
642
|
<div class="banner">
|
|
605
643
|
<div class="banner-inner">
|
|
606
|
-
|
|
644
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
607
645
|
<p>Zero-config REST API mock server</p>
|
|
608
646
|
<div class="banner-meta">
|
|
609
647
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -655,7 +693,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
655
693
|
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
656
694
|
|
|
657
695
|
<footer>
|
|
658
|
-
Powered by <a href="https://github.com/aggiovato/
|
|
696
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
659
697
|
</footer>
|
|
660
698
|
|
|
661
699
|
</div>
|
|
@@ -686,6 +724,10 @@ function nextId(items) {
|
|
|
686
724
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
687
725
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
688
726
|
}
|
|
727
|
+
function generateId(items, strategy) {
|
|
728
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
729
|
+
return nextId(items);
|
|
730
|
+
}
|
|
689
731
|
function firstParam(value) {
|
|
690
732
|
if (value === void 0) return void 0;
|
|
691
733
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -773,10 +815,10 @@ function findById(items, id) {
|
|
|
773
815
|
function findIndexById(items, id) {
|
|
774
816
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
775
817
|
}
|
|
776
|
-
function createItem(storage, resource, body) {
|
|
818
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
777
819
|
const collection = storage.getCollection(resource) ?? [];
|
|
778
820
|
const item = {
|
|
779
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
821
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
780
822
|
...body
|
|
781
823
|
};
|
|
782
824
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -954,13 +996,74 @@ var CollectionRouteCommand = class {
|
|
|
954
996
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
955
997
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
956
998
|
}
|
|
957
|
-
const item = createItem(
|
|
999
|
+
const item = createItem(
|
|
1000
|
+
this.storage,
|
|
1001
|
+
this.resource,
|
|
1002
|
+
req.body,
|
|
1003
|
+
this.options.idStrategy
|
|
1004
|
+
);
|
|
958
1005
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
959
1006
|
});
|
|
960
1007
|
}
|
|
961
1008
|
};
|
|
962
1009
|
|
|
1010
|
+
// src/utils/conditions.ts
|
|
1011
|
+
function resolveRequestPath(dotPath, req) {
|
|
1012
|
+
const [root, ...rest] = dotPath.split(".");
|
|
1013
|
+
let value;
|
|
1014
|
+
switch (root) {
|
|
1015
|
+
case "body":
|
|
1016
|
+
value = req.body;
|
|
1017
|
+
break;
|
|
1018
|
+
case "params":
|
|
1019
|
+
value = req.params;
|
|
1020
|
+
break;
|
|
1021
|
+
case "query":
|
|
1022
|
+
value = req.query;
|
|
1023
|
+
break;
|
|
1024
|
+
case "headers":
|
|
1025
|
+
value = req.headers;
|
|
1026
|
+
break;
|
|
1027
|
+
default:
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
for (const key of rest) {
|
|
1031
|
+
if (value != null && typeof value === "object") {
|
|
1032
|
+
value = value[key];
|
|
1033
|
+
} else {
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return value;
|
|
1038
|
+
}
|
|
1039
|
+
function matchConditionGroup(group, req) {
|
|
1040
|
+
return Object.entries(group).every(([key, expected]) => {
|
|
1041
|
+
const op = OPERATORS.find((o) => key.endsWith(o));
|
|
1042
|
+
if (op) {
|
|
1043
|
+
const path = key.slice(0, -op.length);
|
|
1044
|
+
const value2 = resolveRequestPath(path, req);
|
|
1045
|
+
if (value2 === void 0) return false;
|
|
1046
|
+
return applyOperator(value2, op, String(expected));
|
|
1047
|
+
}
|
|
1048
|
+
const value = resolveRequestPath(key, req);
|
|
1049
|
+
return String(value) === String(expected);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function matchWhen(when, req) {
|
|
1053
|
+
if (Array.isArray(when)) {
|
|
1054
|
+
return when.some((group) => matchConditionGroup(group, req));
|
|
1055
|
+
}
|
|
1056
|
+
return matchConditionGroup(when, req);
|
|
1057
|
+
}
|
|
1058
|
+
function findMatchingScenario(scenarios, req) {
|
|
1059
|
+
return scenarios.find((s) => matchWhen(s.when, req));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
963
1062
|
// src/router/routes/custom.routes.ts
|
|
1063
|
+
function resolveBody(body, ctx) {
|
|
1064
|
+
if (body != null && hasTemplates(body)) return interpolate(body, ctx);
|
|
1065
|
+
return body ?? null;
|
|
1066
|
+
}
|
|
964
1067
|
var CustomRouteCommand = class {
|
|
965
1068
|
constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
|
|
966
1069
|
this.storage = storage;
|
|
@@ -985,6 +1088,13 @@ var CustomRouteCommand = class {
|
|
|
985
1088
|
method,
|
|
986
1089
|
url,
|
|
987
1090
|
handler: async (req, reply) => {
|
|
1091
|
+
if (route.delay && route.delay > 0) {
|
|
1092
|
+
await new Promise((resolve5) => setTimeout(resolve5, route.delay));
|
|
1093
|
+
}
|
|
1094
|
+
if (route.error) {
|
|
1095
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1096
|
+
return reply.status(route.error).send(body2);
|
|
1097
|
+
}
|
|
988
1098
|
for (const [key, value] of Object.entries(headers)) {
|
|
989
1099
|
reply.header(key, value);
|
|
990
1100
|
}
|
|
@@ -994,13 +1104,13 @@ var CustomRouteCommand = class {
|
|
|
994
1104
|
return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
|
|
995
1105
|
}
|
|
996
1106
|
try {
|
|
997
|
-
const
|
|
1107
|
+
const ctx2 = {
|
|
998
1108
|
params: req.params,
|
|
999
1109
|
query: req.query,
|
|
1000
1110
|
body: req.body,
|
|
1001
1111
|
headers: req.headers
|
|
1002
1112
|
};
|
|
1003
|
-
const result = await fn(
|
|
1113
|
+
const result = await fn(ctx2);
|
|
1004
1114
|
const resStatus = result.status ?? 200;
|
|
1005
1115
|
for (const [k, v] of Object.entries(result.headers ?? {})) {
|
|
1006
1116
|
reply.header(k, v);
|
|
@@ -1012,12 +1122,24 @@ var CustomRouteCommand = class {
|
|
|
1012
1122
|
return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
|
|
1013
1123
|
}
|
|
1014
1124
|
}
|
|
1015
|
-
const
|
|
1125
|
+
const ctx = {
|
|
1016
1126
|
params: req.params,
|
|
1017
1127
|
query: req.query,
|
|
1018
1128
|
body: req.body,
|
|
1019
1129
|
headers: req.headers
|
|
1020
|
-
}
|
|
1130
|
+
};
|
|
1131
|
+
if (route.scenarios?.length) {
|
|
1132
|
+
const matched = findMatchingScenario(route.scenarios, ctx);
|
|
1133
|
+
const active = matched?.response ?? route.otherwise;
|
|
1134
|
+
if (active) {
|
|
1135
|
+
const aStatus = active.status ?? 200;
|
|
1136
|
+
const aBody = resolveBody(active.body, ctx);
|
|
1137
|
+
for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
|
|
1138
|
+
if (!active.body && aStatus === 204) return reply.status(aStatus).send();
|
|
1139
|
+
return reply.status(aStatus).send(aBody);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
|
|
1021
1143
|
if (body === null && status === 204) return reply.status(status).send();
|
|
1022
1144
|
return reply.status(status).send(body);
|
|
1023
1145
|
}
|
|
@@ -1269,15 +1391,22 @@ var yrestOptionsSchema = z.object({
|
|
|
1269
1391
|
pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1270
1392
|
enabled: v !== false,
|
|
1271
1393
|
limit: v === false || v === true ? 10 : v
|
|
1272
|
-
}))
|
|
1394
|
+
})),
|
|
1395
|
+
/**
|
|
1396
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1397
|
+
*
|
|
1398
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1399
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1400
|
+
*/
|
|
1401
|
+
idStrategy: z.enum(["increment", "uuid"]).default("increment")
|
|
1273
1402
|
});
|
|
1274
1403
|
|
|
1275
1404
|
// src/config/loadConfigFile.ts
|
|
1276
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1405
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1277
1406
|
import { parse as parse2 } from "yaml";
|
|
1278
1407
|
function loadConfigFile(configPath) {
|
|
1279
1408
|
if (!existsSync2(configPath)) return {};
|
|
1280
|
-
const raw =
|
|
1409
|
+
const raw = readFileSync3(configPath, "utf8");
|
|
1281
1410
|
return parse2(raw) ?? {};
|
|
1282
1411
|
}
|
|
1283
1412
|
|
|
@@ -1314,8 +1443,12 @@ function registerServe(program2) {
|
|
|
1314
1443
|
).option(
|
|
1315
1444
|
"--handlers <file>",
|
|
1316
1445
|
"Path to a JavaScript file exporting custom route handler functions"
|
|
1446
|
+
).option(
|
|
1447
|
+
"--id-strategy <strategy>",
|
|
1448
|
+
"Id generation strategy for new items: increment (default) or uuid",
|
|
1449
|
+
"increment"
|
|
1317
1450
|
).action(async (file, flags, cmd) => {
|
|
1318
|
-
const fileConfig = loadConfigFile(
|
|
1451
|
+
const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
|
|
1319
1452
|
const cliOverrides = Object.fromEntries(
|
|
1320
1453
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
1321
1454
|
);
|
|
@@ -1418,8 +1551,8 @@ function registerServe(program2) {
|
|
|
1418
1551
|
}
|
|
1419
1552
|
|
|
1420
1553
|
// src/cli/commands/handler.ts
|
|
1421
|
-
import { existsSync as existsSync4, readFileSync as
|
|
1422
|
-
import { join as
|
|
1554
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1555
|
+
import { join as join3, resolve as resolve4, basename } from "path";
|
|
1423
1556
|
import { parse as parse3, stringify as stringify2 } from "yaml";
|
|
1424
1557
|
var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
|
|
1425
1558
|
// Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
|
|
@@ -1446,7 +1579,7 @@ function registerHandler(program2) {
|
|
|
1446
1579
|
"--register",
|
|
1447
1580
|
"Also add a _routes entry to db.yml linking this handler to method + path"
|
|
1448
1581
|
).action((name, flags) => {
|
|
1449
|
-
const fileConfig = loadConfigFile(
|
|
1582
|
+
const fileConfig = loadConfigFile(join3(process.cwd(), "yrest.config.yml"));
|
|
1450
1583
|
const handlersPath = resolve4(
|
|
1451
1584
|
fileConfig.handlers ?? "yrest.handlers.js"
|
|
1452
1585
|
);
|
|
@@ -1459,7 +1592,7 @@ function registerHandler(program2) {
|
|
|
1459
1592
|
);
|
|
1460
1593
|
console.log(` Created ${basename(handlersPath)}`);
|
|
1461
1594
|
} else {
|
|
1462
|
-
const existing =
|
|
1595
|
+
const existing = readFileSync4(handlersPath, "utf8");
|
|
1463
1596
|
if (existing.includes(`function ${name}(`)) {
|
|
1464
1597
|
console.error(` Error: handler "${name}" already exists in ${basename(handlersPath)}`);
|
|
1465
1598
|
process.exit(1);
|
|
@@ -1476,7 +1609,7 @@ function registerHandler(program2) {
|
|
|
1476
1609
|
console.error(` Error: database file not found at ${dbPath}`);
|
|
1477
1610
|
process.exit(1);
|
|
1478
1611
|
}
|
|
1479
|
-
const raw = parse3(
|
|
1612
|
+
const raw = parse3(readFileSync4(dbPath, "utf8")) ?? {};
|
|
1480
1613
|
if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
|
|
1481
1614
|
const routes = raw["_routes"];
|
|
1482
1615
|
const alreadyRegistered = routes.some((r) => r["handler"] === name);
|
package/dist/index.d.mts
CHANGED
|
@@ -21,19 +21,72 @@ type Data = Record<string, Resource[]>;
|
|
|
21
21
|
* // GET /users/1/posts → returns posts where userId === "1"
|
|
22
22
|
*/
|
|
23
23
|
type Relations = Record<string, Record<string, string>>;
|
|
24
|
+
/**
|
|
25
|
+
* A static response block shared by {@link CustomRoute} and {@link Scenario}.
|
|
26
|
+
*/
|
|
27
|
+
type RouteResponse = {
|
|
28
|
+
/** HTTP status code. Defaults to `200` if omitted. */
|
|
29
|
+
status?: number;
|
|
30
|
+
/** Response body. Any YAML-serialisable value (object, array, string, number, null). */
|
|
31
|
+
body?: unknown;
|
|
32
|
+
/** Additional response headers to set alongside `Content-Type`. */
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A conditional response variant for a custom route.
|
|
37
|
+
*
|
|
38
|
+
* Evaluated in declaration order — the first scenario whose `when` conditions match wins.
|
|
39
|
+
* If none match, the route falls back to `otherwise:` (if defined) or `response:`.
|
|
40
|
+
*
|
|
41
|
+
* **`when` as an object** — all entries must match (AND):
|
|
42
|
+
* ```yaml
|
|
43
|
+
* when: { body.email: ana@test.com, body.password: secret }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* **`when` as an array** — any group must match (OR of ANDs):
|
|
47
|
+
* ```yaml
|
|
48
|
+
* when:
|
|
49
|
+
* - { body.role: admin }
|
|
50
|
+
* - { body.role: superadmin }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Condition keys use dot-notation (`body.X`, `params.X`, `query.X`, `headers.X`).
|
|
54
|
+
* Field operator suffixes are supported: `_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`.
|
|
55
|
+
* Response bodies support `{{}}` template variables (same as static routes).
|
|
56
|
+
*/
|
|
57
|
+
type Scenario = {
|
|
58
|
+
/**
|
|
59
|
+
* Condition(s) to evaluate against the request.
|
|
60
|
+
* - Object → all entries AND
|
|
61
|
+
* - Array of objects → any group OR (each group is AND internally)
|
|
62
|
+
*/
|
|
63
|
+
when: Record<string, unknown> | Record<string, unknown>[];
|
|
64
|
+
/** Response to return when the conditions match. Supports `{{}}` template variables. */
|
|
65
|
+
response: RouteResponse;
|
|
66
|
+
};
|
|
24
67
|
/**
|
|
25
68
|
* A single custom route declared under `_routes` in the YAML file.
|
|
26
69
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
70
|
+
* Resolution priority per request:
|
|
71
|
+
* 1. `handler` function (if defined and found in the handlers file)
|
|
72
|
+
* 2. First matching `scenario` (evaluated in declaration order)
|
|
73
|
+
* 3. `otherwise` block (explicit fallback when scenarios are defined but none matched)
|
|
74
|
+
* 4. Static `response` block (final fallback)
|
|
29
75
|
*
|
|
30
76
|
* @example
|
|
31
77
|
* // _routes:
|
|
32
78
|
* // - method: POST
|
|
33
79
|
* // path: /login
|
|
34
|
-
* //
|
|
35
|
-
* //
|
|
36
|
-
* //
|
|
80
|
+
* // scenarios:
|
|
81
|
+
* // - when: { body.password: secret }
|
|
82
|
+
* // response: { status: 200, body: { token: real-tok } }
|
|
83
|
+
* // - when:
|
|
84
|
+
* // - { body.role: admin }
|
|
85
|
+
* // - { body.role: superadmin }
|
|
86
|
+
* // response: { status: 200, body: { token: admin-tok } }
|
|
87
|
+
* // otherwise:
|
|
88
|
+
* // status: 401
|
|
89
|
+
* // body: { error: Invalid credentials }
|
|
37
90
|
*/
|
|
38
91
|
type CustomRoute = {
|
|
39
92
|
/** HTTP method (case-insensitive: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). */
|
|
@@ -42,19 +95,36 @@ type CustomRoute = {
|
|
|
42
95
|
path: string;
|
|
43
96
|
/**
|
|
44
97
|
* Name of an exported function in the handlers file (`handlers:` in config).
|
|
45
|
-
*
|
|
46
|
-
* Takes priority over `response:`. Falls back to `response:` if the name is not found.
|
|
98
|
+
* Takes priority over `scenarios` and `response`. Falls back to `response` if not found.
|
|
47
99
|
*/
|
|
48
100
|
handler?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
/** Conditional response variants. Evaluated in order — first match wins. */
|
|
102
|
+
scenarios?: Scenario[];
|
|
103
|
+
/**
|
|
104
|
+
* Explicit fallback response when `scenarios` are defined but none matched.
|
|
105
|
+
* Takes priority over `response` when present. Supports `{{}}` template variables.
|
|
106
|
+
*/
|
|
107
|
+
otherwise?: RouteResponse;
|
|
108
|
+
/** Static or template response. Final fallback when no handler, scenario, or otherwise applies. */
|
|
109
|
+
response?: RouteResponse;
|
|
110
|
+
/**
|
|
111
|
+
* Per-route response delay in milliseconds. Overrides the global `--delay` option for this route.
|
|
112
|
+
* Applied before any response is sent, regardless of which path resolved the response.
|
|
113
|
+
*/
|
|
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;
|
|
58
128
|
};
|
|
59
129
|
/**
|
|
60
130
|
* In-memory store backed by a YAML file.
|
|
@@ -194,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
194
264
|
enabled: boolean;
|
|
195
265
|
limit: number;
|
|
196
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"]>>;
|
|
197
274
|
}, "strip", z.ZodTypeAny, {
|
|
198
275
|
file: string;
|
|
199
276
|
port: number;
|
|
@@ -207,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
207
284
|
enabled: boolean;
|
|
208
285
|
limit: number;
|
|
209
286
|
};
|
|
287
|
+
idStrategy: "increment" | "uuid";
|
|
210
288
|
handlers?: string | undefined;
|
|
211
289
|
}, {
|
|
212
290
|
file: string;
|
|
@@ -219,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
219
297
|
delay?: number | undefined;
|
|
220
298
|
handlers?: string | undefined;
|
|
221
299
|
pageable?: number | boolean | undefined;
|
|
300
|
+
idStrategy?: "increment" | "uuid" | undefined;
|
|
222
301
|
}>;
|
|
223
302
|
/**
|
|
224
303
|
* Resolved server configuration after Zod validation and transformation.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,19 +21,72 @@ type Data = Record<string, Resource[]>;
|
|
|
21
21
|
* // GET /users/1/posts → returns posts where userId === "1"
|
|
22
22
|
*/
|
|
23
23
|
type Relations = Record<string, Record<string, string>>;
|
|
24
|
+
/**
|
|
25
|
+
* A static response block shared by {@link CustomRoute} and {@link Scenario}.
|
|
26
|
+
*/
|
|
27
|
+
type RouteResponse = {
|
|
28
|
+
/** HTTP status code. Defaults to `200` if omitted. */
|
|
29
|
+
status?: number;
|
|
30
|
+
/** Response body. Any YAML-serialisable value (object, array, string, number, null). */
|
|
31
|
+
body?: unknown;
|
|
32
|
+
/** Additional response headers to set alongside `Content-Type`. */
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A conditional response variant for a custom route.
|
|
37
|
+
*
|
|
38
|
+
* Evaluated in declaration order — the first scenario whose `when` conditions match wins.
|
|
39
|
+
* If none match, the route falls back to `otherwise:` (if defined) or `response:`.
|
|
40
|
+
*
|
|
41
|
+
* **`when` as an object** — all entries must match (AND):
|
|
42
|
+
* ```yaml
|
|
43
|
+
* when: { body.email: ana@test.com, body.password: secret }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* **`when` as an array** — any group must match (OR of ANDs):
|
|
47
|
+
* ```yaml
|
|
48
|
+
* when:
|
|
49
|
+
* - { body.role: admin }
|
|
50
|
+
* - { body.role: superadmin }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Condition keys use dot-notation (`body.X`, `params.X`, `query.X`, `headers.X`).
|
|
54
|
+
* Field operator suffixes are supported: `_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`.
|
|
55
|
+
* Response bodies support `{{}}` template variables (same as static routes).
|
|
56
|
+
*/
|
|
57
|
+
type Scenario = {
|
|
58
|
+
/**
|
|
59
|
+
* Condition(s) to evaluate against the request.
|
|
60
|
+
* - Object → all entries AND
|
|
61
|
+
* - Array of objects → any group OR (each group is AND internally)
|
|
62
|
+
*/
|
|
63
|
+
when: Record<string, unknown> | Record<string, unknown>[];
|
|
64
|
+
/** Response to return when the conditions match. Supports `{{}}` template variables. */
|
|
65
|
+
response: RouteResponse;
|
|
66
|
+
};
|
|
24
67
|
/**
|
|
25
68
|
* A single custom route declared under `_routes` in the YAML file.
|
|
26
69
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
70
|
+
* Resolution priority per request:
|
|
71
|
+
* 1. `handler` function (if defined and found in the handlers file)
|
|
72
|
+
* 2. First matching `scenario` (evaluated in declaration order)
|
|
73
|
+
* 3. `otherwise` block (explicit fallback when scenarios are defined but none matched)
|
|
74
|
+
* 4. Static `response` block (final fallback)
|
|
29
75
|
*
|
|
30
76
|
* @example
|
|
31
77
|
* // _routes:
|
|
32
78
|
* // - method: POST
|
|
33
79
|
* // path: /login
|
|
34
|
-
* //
|
|
35
|
-
* //
|
|
36
|
-
* //
|
|
80
|
+
* // scenarios:
|
|
81
|
+
* // - when: { body.password: secret }
|
|
82
|
+
* // response: { status: 200, body: { token: real-tok } }
|
|
83
|
+
* // - when:
|
|
84
|
+
* // - { body.role: admin }
|
|
85
|
+
* // - { body.role: superadmin }
|
|
86
|
+
* // response: { status: 200, body: { token: admin-tok } }
|
|
87
|
+
* // otherwise:
|
|
88
|
+
* // status: 401
|
|
89
|
+
* // body: { error: Invalid credentials }
|
|
37
90
|
*/
|
|
38
91
|
type CustomRoute = {
|
|
39
92
|
/** HTTP method (case-insensitive: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). */
|
|
@@ -42,19 +95,36 @@ type CustomRoute = {
|
|
|
42
95
|
path: string;
|
|
43
96
|
/**
|
|
44
97
|
* Name of an exported function in the handlers file (`handlers:` in config).
|
|
45
|
-
*
|
|
46
|
-
* Takes priority over `response:`. Falls back to `response:` if the name is not found.
|
|
98
|
+
* Takes priority over `scenarios` and `response`. Falls back to `response` if not found.
|
|
47
99
|
*/
|
|
48
100
|
handler?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
/** Conditional response variants. Evaluated in order — first match wins. */
|
|
102
|
+
scenarios?: Scenario[];
|
|
103
|
+
/**
|
|
104
|
+
* Explicit fallback response when `scenarios` are defined but none matched.
|
|
105
|
+
* Takes priority over `response` when present. Supports `{{}}` template variables.
|
|
106
|
+
*/
|
|
107
|
+
otherwise?: RouteResponse;
|
|
108
|
+
/** Static or template response. Final fallback when no handler, scenario, or otherwise applies. */
|
|
109
|
+
response?: RouteResponse;
|
|
110
|
+
/**
|
|
111
|
+
* Per-route response delay in milliseconds. Overrides the global `--delay` option for this route.
|
|
112
|
+
* Applied before any response is sent, regardless of which path resolved the response.
|
|
113
|
+
*/
|
|
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;
|
|
58
128
|
};
|
|
59
129
|
/**
|
|
60
130
|
* In-memory store backed by a YAML file.
|
|
@@ -194,6 +264,13 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
194
264
|
enabled: boolean;
|
|
195
265
|
limit: number;
|
|
196
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"]>>;
|
|
197
274
|
}, "strip", z.ZodTypeAny, {
|
|
198
275
|
file: string;
|
|
199
276
|
port: number;
|
|
@@ -207,6 +284,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
207
284
|
enabled: boolean;
|
|
208
285
|
limit: number;
|
|
209
286
|
};
|
|
287
|
+
idStrategy: "increment" | "uuid";
|
|
210
288
|
handlers?: string | undefined;
|
|
211
289
|
}, {
|
|
212
290
|
file: string;
|
|
@@ -219,6 +297,7 @@ declare const yrestOptionsSchema: z.ZodObject<{
|
|
|
219
297
|
delay?: number | undefined;
|
|
220
298
|
handlers?: string | undefined;
|
|
221
299
|
pageable?: number | boolean | undefined;
|
|
300
|
+
idStrategy?: "increment" | "uuid" | undefined;
|
|
222
301
|
}>;
|
|
223
302
|
/**
|
|
224
303
|
* Resolved server configuration after Zod validation and transformation.
|