@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/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,7 +197,7 @@ 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();
|
|
@@ -237,6 +240,9 @@ init_cjs_shims();
|
|
|
237
240
|
|
|
238
241
|
// src/router/templates/about.template.ts
|
|
239
242
|
init_cjs_shims();
|
|
243
|
+
var import_node_fs2 = require("fs");
|
|
244
|
+
var import_node_path = require("path");
|
|
245
|
+
var import_node_url = require("url");
|
|
240
246
|
|
|
241
247
|
// src/utils/interpolate.ts
|
|
242
248
|
init_cjs_shims();
|
|
@@ -286,6 +292,15 @@ function hasTemplates(value) {
|
|
|
286
292
|
}
|
|
287
293
|
|
|
288
294
|
// src/router/templates/about.template.ts
|
|
295
|
+
var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
296
|
+
var LOGO_SRC = (() => {
|
|
297
|
+
try {
|
|
298
|
+
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/logo-color.png"));
|
|
299
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
300
|
+
} catch {
|
|
301
|
+
return "";
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
289
304
|
var METHOD_COLOR = {
|
|
290
305
|
GET: "#3fb950",
|
|
291
306
|
POST: "#58a6ff",
|
|
@@ -444,6 +459,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
444
459
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
445
460
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
446
461
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
462
|
+
if (options.idStrategy !== "increment")
|
|
463
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
447
464
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
448
465
|
const nestedRows = [];
|
|
449
466
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -485,16 +502,38 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
485
502
|
<table><tbody>
|
|
486
503
|
${customRoutes.map((r) => {
|
|
487
504
|
const fullPath = `${base}${r.path}`;
|
|
505
|
+
const tags = [];
|
|
506
|
+
if (r.error) {
|
|
507
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
508
|
+
}
|
|
509
|
+
if (r.delay && r.delay > 0) {
|
|
510
|
+
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
511
|
+
}
|
|
512
|
+
if (r.scenarios?.length) {
|
|
513
|
+
const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
|
|
514
|
+
tags.push(
|
|
515
|
+
`<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (r.otherwise) {
|
|
519
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
520
|
+
}
|
|
488
521
|
let desc;
|
|
489
|
-
if (r.
|
|
522
|
+
if (r.error) {
|
|
523
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
524
|
+
} else if (r.handler) {
|
|
490
525
|
const found = handlers.has(r.handler);
|
|
491
526
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
527
|
+
} else if (r.scenarios?.length) {
|
|
528
|
+
const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
|
|
529
|
+
desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
|
|
492
530
|
} else if (r.response?.body != null && hasTemplates(r.response.body)) {
|
|
493
|
-
desc = `Dynamic
|
|
531
|
+
desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
|
|
494
532
|
} else {
|
|
495
533
|
const status = r.response?.status ?? 200;
|
|
496
|
-
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` +
|
|
534
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
497
535
|
}
|
|
536
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
498
537
|
return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
|
|
499
538
|
}).join("")}
|
|
500
539
|
</tbody></table>
|
|
@@ -646,7 +685,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
646
685
|
|
|
647
686
|
<div class="banner">
|
|
648
687
|
<div class="banner-inner">
|
|
649
|
-
|
|
688
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
650
689
|
<p>Zero-config REST API mock server</p>
|
|
651
690
|
<div class="banner-meta">
|
|
652
691
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -698,7 +737,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
698
737
|
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
699
738
|
|
|
700
739
|
<footer>
|
|
701
|
-
Powered by <a href="https://github.com/aggiovato/
|
|
740
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
702
741
|
</footer>
|
|
703
742
|
|
|
704
743
|
</div>
|
|
@@ -733,6 +772,10 @@ function nextId(items) {
|
|
|
733
772
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
734
773
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
735
774
|
}
|
|
775
|
+
function generateId(items, strategy) {
|
|
776
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
777
|
+
return nextId(items);
|
|
778
|
+
}
|
|
736
779
|
function firstParam(value) {
|
|
737
780
|
if (value === void 0) return void 0;
|
|
738
781
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -822,10 +865,10 @@ function findById(items, id) {
|
|
|
822
865
|
function findIndexById(items, id) {
|
|
823
866
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
824
867
|
}
|
|
825
|
-
function createItem(storage, resource, body) {
|
|
868
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
826
869
|
const collection = storage.getCollection(resource) ?? [];
|
|
827
870
|
const item = {
|
|
828
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
871
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
829
872
|
...body
|
|
830
873
|
};
|
|
831
874
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -1004,7 +1047,12 @@ var CollectionRouteCommand = class {
|
|
|
1004
1047
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
1005
1048
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
1006
1049
|
}
|
|
1007
|
-
const item = createItem(
|
|
1050
|
+
const item = createItem(
|
|
1051
|
+
this.storage,
|
|
1052
|
+
this.resource,
|
|
1053
|
+
req.body,
|
|
1054
|
+
this.options.idStrategy
|
|
1055
|
+
);
|
|
1008
1056
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
1009
1057
|
});
|
|
1010
1058
|
}
|
|
@@ -1012,6 +1060,65 @@ var CollectionRouteCommand = class {
|
|
|
1012
1060
|
|
|
1013
1061
|
// src/router/routes/custom.routes.ts
|
|
1014
1062
|
init_cjs_shims();
|
|
1063
|
+
|
|
1064
|
+
// src/utils/conditions.ts
|
|
1065
|
+
init_cjs_shims();
|
|
1066
|
+
function resolveRequestPath(dotPath, req) {
|
|
1067
|
+
const [root, ...rest] = dotPath.split(".");
|
|
1068
|
+
let value;
|
|
1069
|
+
switch (root) {
|
|
1070
|
+
case "body":
|
|
1071
|
+
value = req.body;
|
|
1072
|
+
break;
|
|
1073
|
+
case "params":
|
|
1074
|
+
value = req.params;
|
|
1075
|
+
break;
|
|
1076
|
+
case "query":
|
|
1077
|
+
value = req.query;
|
|
1078
|
+
break;
|
|
1079
|
+
case "headers":
|
|
1080
|
+
value = req.headers;
|
|
1081
|
+
break;
|
|
1082
|
+
default:
|
|
1083
|
+
return void 0;
|
|
1084
|
+
}
|
|
1085
|
+
for (const key of rest) {
|
|
1086
|
+
if (value != null && typeof value === "object") {
|
|
1087
|
+
value = value[key];
|
|
1088
|
+
} else {
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return value;
|
|
1093
|
+
}
|
|
1094
|
+
function matchConditionGroup(group, req) {
|
|
1095
|
+
return Object.entries(group).every(([key, expected]) => {
|
|
1096
|
+
const op = OPERATORS.find((o) => key.endsWith(o));
|
|
1097
|
+
if (op) {
|
|
1098
|
+
const path = key.slice(0, -op.length);
|
|
1099
|
+
const value2 = resolveRequestPath(path, req);
|
|
1100
|
+
if (value2 === void 0) return false;
|
|
1101
|
+
return applyOperator(value2, op, String(expected));
|
|
1102
|
+
}
|
|
1103
|
+
const value = resolveRequestPath(key, req);
|
|
1104
|
+
return String(value) === String(expected);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function matchWhen(when, req) {
|
|
1108
|
+
if (Array.isArray(when)) {
|
|
1109
|
+
return when.some((group) => matchConditionGroup(group, req));
|
|
1110
|
+
}
|
|
1111
|
+
return matchConditionGroup(when, req);
|
|
1112
|
+
}
|
|
1113
|
+
function findMatchingScenario(scenarios, req) {
|
|
1114
|
+
return scenarios.find((s) => matchWhen(s.when, req));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/router/routes/custom.routes.ts
|
|
1118
|
+
function resolveBody(body, ctx) {
|
|
1119
|
+
if (body != null && hasTemplates(body)) return interpolate(body, ctx);
|
|
1120
|
+
return body ?? null;
|
|
1121
|
+
}
|
|
1015
1122
|
var CustomRouteCommand = class {
|
|
1016
1123
|
constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
|
|
1017
1124
|
this.storage = storage;
|
|
@@ -1036,6 +1143,13 @@ var CustomRouteCommand = class {
|
|
|
1036
1143
|
method,
|
|
1037
1144
|
url,
|
|
1038
1145
|
handler: async (req, reply) => {
|
|
1146
|
+
if (route.delay && route.delay > 0) {
|
|
1147
|
+
await new Promise((resolve3) => setTimeout(resolve3, route.delay));
|
|
1148
|
+
}
|
|
1149
|
+
if (route.error) {
|
|
1150
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1151
|
+
return reply.status(route.error).send(body2);
|
|
1152
|
+
}
|
|
1039
1153
|
for (const [key, value] of Object.entries(headers)) {
|
|
1040
1154
|
reply.header(key, value);
|
|
1041
1155
|
}
|
|
@@ -1045,13 +1159,13 @@ var CustomRouteCommand = class {
|
|
|
1045
1159
|
return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
|
|
1046
1160
|
}
|
|
1047
1161
|
try {
|
|
1048
|
-
const
|
|
1162
|
+
const ctx2 = {
|
|
1049
1163
|
params: req.params,
|
|
1050
1164
|
query: req.query,
|
|
1051
1165
|
body: req.body,
|
|
1052
1166
|
headers: req.headers
|
|
1053
1167
|
};
|
|
1054
|
-
const result = await fn(
|
|
1168
|
+
const result = await fn(ctx2);
|
|
1055
1169
|
const resStatus = result.status ?? 200;
|
|
1056
1170
|
for (const [k, v] of Object.entries(result.headers ?? {})) {
|
|
1057
1171
|
reply.header(k, v);
|
|
@@ -1063,12 +1177,24 @@ var CustomRouteCommand = class {
|
|
|
1063
1177
|
return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
|
|
1064
1178
|
}
|
|
1065
1179
|
}
|
|
1066
|
-
const
|
|
1180
|
+
const ctx = {
|
|
1067
1181
|
params: req.params,
|
|
1068
1182
|
query: req.query,
|
|
1069
1183
|
body: req.body,
|
|
1070
1184
|
headers: req.headers
|
|
1071
|
-
}
|
|
1185
|
+
};
|
|
1186
|
+
if (route.scenarios?.length) {
|
|
1187
|
+
const matched = findMatchingScenario(route.scenarios, ctx);
|
|
1188
|
+
const active = matched?.response ?? route.otherwise;
|
|
1189
|
+
if (active) {
|
|
1190
|
+
const aStatus = active.status ?? 200;
|
|
1191
|
+
const aBody = resolveBody(active.body, ctx);
|
|
1192
|
+
for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
|
|
1193
|
+
if (!active.body && aStatus === 204) return reply.status(aStatus).send();
|
|
1194
|
+
return reply.status(aStatus).send(aBody);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
|
|
1072
1198
|
if (body === null && status === 204) return reply.status(status).send();
|
|
1073
1199
|
return reply.status(status).send(body);
|
|
1074
1200
|
}
|
|
@@ -1294,7 +1420,7 @@ function createYrestServer(options) {
|
|
|
1294
1420
|
return {
|
|
1295
1421
|
async start() {
|
|
1296
1422
|
const storage = "data" in options && options.data !== void 0 ? createInMemoryStorage(options.data) : (await Promise.resolve().then(() => (init_yrestStorage(), yrestStorage_exports))).createYrestStorage(resolvedOptions.file);
|
|
1297
|
-
const handlers = resolvedOptions.handlers ? await loadHandlers((0,
|
|
1423
|
+
const handlers = resolvedOptions.handlers ? await loadHandlers((0, import_node_path3.resolve)(resolvedOptions.handlers)) : /* @__PURE__ */ new Map();
|
|
1298
1424
|
_inner = createYrestServerFromStorage(storage, resolvedOptions, handlers);
|
|
1299
1425
|
await _inner.start();
|
|
1300
1426
|
},
|
|
@@ -1321,7 +1447,8 @@ function buildOptions(opts) {
|
|
|
1321
1447
|
readonly: opts.readonly ?? false,
|
|
1322
1448
|
delay: opts.delay ?? 0,
|
|
1323
1449
|
handlers: opts.handlers,
|
|
1324
|
-
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
|
|
1450
|
+
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
|
|
1451
|
+
idStrategy: "increment"
|
|
1325
1452
|
};
|
|
1326
1453
|
}
|
|
1327
1454
|
function createInMemoryStorage(data) {
|
|
@@ -1405,7 +1532,14 @@ var yrestOptionsSchema = import_zod.z.object({
|
|
|
1405
1532
|
pageable: import_zod.z.union([import_zod.z.boolean(), import_zod.z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1406
1533
|
enabled: v !== false,
|
|
1407
1534
|
limit: v === false || v === true ? 10 : v
|
|
1408
|
-
}))
|
|
1535
|
+
})),
|
|
1536
|
+
/**
|
|
1537
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1538
|
+
*
|
|
1539
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1540
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1541
|
+
*/
|
|
1542
|
+
idStrategy: import_zod.z.enum(["increment", "uuid"]).default("increment")
|
|
1409
1543
|
});
|
|
1410
1544
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1411
1545
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -35,13 +35,13 @@ var yrestStorage_exports = {};
|
|
|
35
35
|
__export(yrestStorage_exports, {
|
|
36
36
|
createYrestStorage: () => createYrestStorage
|
|
37
37
|
});
|
|
38
|
-
import { readFileSync, writeFileSync, renameSync } from "fs";
|
|
39
|
-
import { resolve, dirname } from "path";
|
|
38
|
+
import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
39
|
+
import { resolve, dirname as dirname2 } from "path";
|
|
40
40
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
41
41
|
import { parse as parse2, stringify } from "yaml";
|
|
42
42
|
function createYrestStorage(filePath) {
|
|
43
43
|
const absPath = resolve(filePath);
|
|
44
|
-
const raw = parse2(
|
|
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")
|
|
@@ -206,6 +206,9 @@ init_esm_shims();
|
|
|
206
206
|
|
|
207
207
|
// src/router/templates/about.template.ts
|
|
208
208
|
init_esm_shims();
|
|
209
|
+
import { readFileSync } from "fs";
|
|
210
|
+
import { dirname, join } from "path";
|
|
211
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
209
212
|
|
|
210
213
|
// src/utils/interpolate.ts
|
|
211
214
|
init_esm_shims();
|
|
@@ -255,6 +258,15 @@ function hasTemplates(value) {
|
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
// src/router/templates/about.template.ts
|
|
261
|
+
var _dir = dirname(fileURLToPath2(import.meta.url));
|
|
262
|
+
var LOGO_SRC = (() => {
|
|
263
|
+
try {
|
|
264
|
+
const buf = readFileSync(join(_dir, "../../assets/logo-color.png"));
|
|
265
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
266
|
+
} catch {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
258
270
|
var METHOD_COLOR = {
|
|
259
271
|
GET: "#3fb950",
|
|
260
272
|
POST: "#58a6ff",
|
|
@@ -413,6 +425,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
413
425
|
modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
|
|
414
426
|
if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
|
|
415
427
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
428
|
+
if (options.idStrategy !== "increment")
|
|
429
|
+
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
416
430
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
417
431
|
const nestedRows = [];
|
|
418
432
|
for (const [child, fields] of Object.entries(relations)) {
|
|
@@ -454,16 +468,38 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
454
468
|
<table><tbody>
|
|
455
469
|
${customRoutes.map((r) => {
|
|
456
470
|
const fullPath = `${base}${r.path}`;
|
|
471
|
+
const tags = [];
|
|
472
|
+
if (r.error) {
|
|
473
|
+
tags.push(`<span style="color:#f85149;font-size:11px">error\xB7${r.error}</span>`);
|
|
474
|
+
}
|
|
475
|
+
if (r.delay && r.delay > 0) {
|
|
476
|
+
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
477
|
+
}
|
|
478
|
+
if (r.scenarios?.length) {
|
|
479
|
+
const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
|
|
480
|
+
tags.push(
|
|
481
|
+
`<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (r.otherwise) {
|
|
485
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
486
|
+
}
|
|
457
487
|
let desc;
|
|
458
|
-
if (r.
|
|
488
|
+
if (r.error) {
|
|
489
|
+
desc = `Error injection \u2014 <code>${r.error}</code>`;
|
|
490
|
+
} else if (r.handler) {
|
|
459
491
|
const found = handlers.has(r.handler);
|
|
460
492
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
493
|
+
} else if (r.scenarios?.length) {
|
|
494
|
+
const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
|
|
495
|
+
desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
|
|
461
496
|
} else if (r.response?.body != null && hasTemplates(r.response.body)) {
|
|
462
|
-
desc = `Dynamic
|
|
497
|
+
desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
|
|
463
498
|
} else {
|
|
464
499
|
const status = r.response?.status ?? 200;
|
|
465
|
-
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` +
|
|
500
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
466
501
|
}
|
|
502
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
467
503
|
return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
|
|
468
504
|
}).join("")}
|
|
469
505
|
</tbody></table>
|
|
@@ -615,7 +651,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
615
651
|
|
|
616
652
|
<div class="banner">
|
|
617
653
|
<div class="banner-inner">
|
|
618
|
-
|
|
654
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="68" style="display:block;margin-bottom:0px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
619
655
|
<p>Zero-config REST API mock server</p>
|
|
620
656
|
<div class="banner-meta">
|
|
621
657
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -667,7 +703,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
667
703
|
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
668
704
|
|
|
669
705
|
<footer>
|
|
670
|
-
Powered by <a href="https://github.com/aggiovato/
|
|
706
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
671
707
|
</footer>
|
|
672
708
|
|
|
673
709
|
</div>
|
|
@@ -702,6 +738,10 @@ function nextId(items) {
|
|
|
702
738
|
const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
|
|
703
739
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
704
740
|
}
|
|
741
|
+
function generateId(items, strategy) {
|
|
742
|
+
if (strategy === "uuid") return crypto.randomUUID();
|
|
743
|
+
return nextId(items);
|
|
744
|
+
}
|
|
705
745
|
function firstParam(value) {
|
|
706
746
|
if (value === void 0) return void 0;
|
|
707
747
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -791,10 +831,10 @@ function findById(items, id) {
|
|
|
791
831
|
function findIndexById(items, id) {
|
|
792
832
|
return items.findIndex((i) => String(i["id"]) === id);
|
|
793
833
|
}
|
|
794
|
-
function createItem(storage, resource, body) {
|
|
834
|
+
function createItem(storage, resource, body, idStrategy = "increment") {
|
|
795
835
|
const collection = storage.getCollection(resource) ?? [];
|
|
796
836
|
const item = {
|
|
797
|
-
id: body["id"] !== void 0 ? body["id"] :
|
|
837
|
+
id: body["id"] !== void 0 ? body["id"] : generateId(collection, idStrategy),
|
|
798
838
|
...body
|
|
799
839
|
};
|
|
800
840
|
storage.setCollection(resource, [...collection, item]);
|
|
@@ -973,7 +1013,12 @@ var CollectionRouteCommand = class {
|
|
|
973
1013
|
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
|
|
974
1014
|
return reply.status(400).send({ error: "Request body must be a JSON object" });
|
|
975
1015
|
}
|
|
976
|
-
const item = createItem(
|
|
1016
|
+
const item = createItem(
|
|
1017
|
+
this.storage,
|
|
1018
|
+
this.resource,
|
|
1019
|
+
req.body,
|
|
1020
|
+
this.options.idStrategy
|
|
1021
|
+
);
|
|
977
1022
|
return reply.status(201).send(expandItems(item, req.query, this.resource, this.storage));
|
|
978
1023
|
});
|
|
979
1024
|
}
|
|
@@ -981,6 +1026,65 @@ var CollectionRouteCommand = class {
|
|
|
981
1026
|
|
|
982
1027
|
// src/router/routes/custom.routes.ts
|
|
983
1028
|
init_esm_shims();
|
|
1029
|
+
|
|
1030
|
+
// src/utils/conditions.ts
|
|
1031
|
+
init_esm_shims();
|
|
1032
|
+
function resolveRequestPath(dotPath, req) {
|
|
1033
|
+
const [root, ...rest] = dotPath.split(".");
|
|
1034
|
+
let value;
|
|
1035
|
+
switch (root) {
|
|
1036
|
+
case "body":
|
|
1037
|
+
value = req.body;
|
|
1038
|
+
break;
|
|
1039
|
+
case "params":
|
|
1040
|
+
value = req.params;
|
|
1041
|
+
break;
|
|
1042
|
+
case "query":
|
|
1043
|
+
value = req.query;
|
|
1044
|
+
break;
|
|
1045
|
+
case "headers":
|
|
1046
|
+
value = req.headers;
|
|
1047
|
+
break;
|
|
1048
|
+
default:
|
|
1049
|
+
return void 0;
|
|
1050
|
+
}
|
|
1051
|
+
for (const key of rest) {
|
|
1052
|
+
if (value != null && typeof value === "object") {
|
|
1053
|
+
value = value[key];
|
|
1054
|
+
} else {
|
|
1055
|
+
return void 0;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return value;
|
|
1059
|
+
}
|
|
1060
|
+
function matchConditionGroup(group, req) {
|
|
1061
|
+
return Object.entries(group).every(([key, expected]) => {
|
|
1062
|
+
const op = OPERATORS.find((o) => key.endsWith(o));
|
|
1063
|
+
if (op) {
|
|
1064
|
+
const path2 = key.slice(0, -op.length);
|
|
1065
|
+
const value2 = resolveRequestPath(path2, req);
|
|
1066
|
+
if (value2 === void 0) return false;
|
|
1067
|
+
return applyOperator(value2, op, String(expected));
|
|
1068
|
+
}
|
|
1069
|
+
const value = resolveRequestPath(key, req);
|
|
1070
|
+
return String(value) === String(expected);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function matchWhen(when, req) {
|
|
1074
|
+
if (Array.isArray(when)) {
|
|
1075
|
+
return when.some((group) => matchConditionGroup(group, req));
|
|
1076
|
+
}
|
|
1077
|
+
return matchConditionGroup(when, req);
|
|
1078
|
+
}
|
|
1079
|
+
function findMatchingScenario(scenarios, req) {
|
|
1080
|
+
return scenarios.find((s) => matchWhen(s.when, req));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/router/routes/custom.routes.ts
|
|
1084
|
+
function resolveBody(body, ctx) {
|
|
1085
|
+
if (body != null && hasTemplates(body)) return interpolate(body, ctx);
|
|
1086
|
+
return body ?? null;
|
|
1087
|
+
}
|
|
984
1088
|
var CustomRouteCommand = class {
|
|
985
1089
|
constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
|
|
986
1090
|
this.storage = storage;
|
|
@@ -1005,6 +1109,13 @@ var CustomRouteCommand = class {
|
|
|
1005
1109
|
method,
|
|
1006
1110
|
url,
|
|
1007
1111
|
handler: async (req, reply) => {
|
|
1112
|
+
if (route.delay && route.delay > 0) {
|
|
1113
|
+
await new Promise((resolve3) => setTimeout(resolve3, route.delay));
|
|
1114
|
+
}
|
|
1115
|
+
if (route.error) {
|
|
1116
|
+
const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
|
|
1117
|
+
return reply.status(route.error).send(body2);
|
|
1118
|
+
}
|
|
1008
1119
|
for (const [key, value] of Object.entries(headers)) {
|
|
1009
1120
|
reply.header(key, value);
|
|
1010
1121
|
}
|
|
@@ -1014,13 +1125,13 @@ var CustomRouteCommand = class {
|
|
|
1014
1125
|
return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
|
|
1015
1126
|
}
|
|
1016
1127
|
try {
|
|
1017
|
-
const
|
|
1128
|
+
const ctx2 = {
|
|
1018
1129
|
params: req.params,
|
|
1019
1130
|
query: req.query,
|
|
1020
1131
|
body: req.body,
|
|
1021
1132
|
headers: req.headers
|
|
1022
1133
|
};
|
|
1023
|
-
const result = await fn(
|
|
1134
|
+
const result = await fn(ctx2);
|
|
1024
1135
|
const resStatus = result.status ?? 200;
|
|
1025
1136
|
for (const [k, v] of Object.entries(result.headers ?? {})) {
|
|
1026
1137
|
reply.header(k, v);
|
|
@@ -1032,12 +1143,24 @@ var CustomRouteCommand = class {
|
|
|
1032
1143
|
return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
|
|
1033
1144
|
}
|
|
1034
1145
|
}
|
|
1035
|
-
const
|
|
1146
|
+
const ctx = {
|
|
1036
1147
|
params: req.params,
|
|
1037
1148
|
query: req.query,
|
|
1038
1149
|
body: req.body,
|
|
1039
1150
|
headers: req.headers
|
|
1040
|
-
}
|
|
1151
|
+
};
|
|
1152
|
+
if (route.scenarios?.length) {
|
|
1153
|
+
const matched = findMatchingScenario(route.scenarios, ctx);
|
|
1154
|
+
const active = matched?.response ?? route.otherwise;
|
|
1155
|
+
if (active) {
|
|
1156
|
+
const aStatus = active.status ?? 200;
|
|
1157
|
+
const aBody = resolveBody(active.body, ctx);
|
|
1158
|
+
for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
|
|
1159
|
+
if (!active.body && aStatus === 204) return reply.status(aStatus).send();
|
|
1160
|
+
return reply.status(aStatus).send(aBody);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
|
|
1041
1164
|
if (body === null && status === 204) return reply.status(status).send();
|
|
1042
1165
|
return reply.status(status).send(body);
|
|
1043
1166
|
}
|
|
@@ -1290,7 +1413,8 @@ function buildOptions(opts) {
|
|
|
1290
1413
|
readonly: opts.readonly ?? false,
|
|
1291
1414
|
delay: opts.delay ?? 0,
|
|
1292
1415
|
handlers: opts.handlers,
|
|
1293
|
-
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 }
|
|
1416
|
+
pageable: typeof pageable === "number" ? { enabled: true, limit: pageable } : pageable ? { enabled: true, limit: 10 } : { enabled: false, limit: 10 },
|
|
1417
|
+
idStrategy: "increment"
|
|
1294
1418
|
};
|
|
1295
1419
|
}
|
|
1296
1420
|
function createInMemoryStorage(data) {
|
|
@@ -1374,7 +1498,14 @@ var yrestOptionsSchema = z.object({
|
|
|
1374
1498
|
pageable: z.union([z.boolean(), z.coerce.number().int().positive()]).default(false).transform((v) => ({
|
|
1375
1499
|
enabled: v !== false,
|
|
1376
1500
|
limit: v === false || v === true ? 10 : v
|
|
1377
|
-
}))
|
|
1501
|
+
})),
|
|
1502
|
+
/**
|
|
1503
|
+
* Strategy used to generate `id` values for new items when no `id` is provided in the body.
|
|
1504
|
+
*
|
|
1505
|
+
* - `"increment"` (default) — next integer above the current max id in the collection.
|
|
1506
|
+
* - `"uuid"` — a random UUID v4 string (`crypto.randomUUID()`).
|
|
1507
|
+
*/
|
|
1508
|
+
idStrategy: z.enum(["increment", "uuid"]).default("increment")
|
|
1378
1509
|
});
|
|
1379
1510
|
export {
|
|
1380
1511
|
createServer,
|