@yrest/cli 0.5.3 → 0.7.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/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/cli/index.js +133 -17
- package/dist/cli/index.mjs +133 -17
- package/dist/index.d.mts +205 -30
- package/dist/index.d.ts +205 -30
- package/dist/index.js +363 -33
- package/dist/index.mjs +378 -41
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -108,7 +108,7 @@ function registerInit(program2) {
|
|
|
108
108
|
import { watchFile } from "fs";
|
|
109
109
|
import { join, resolve as resolve3 } from "path";
|
|
110
110
|
|
|
111
|
-
// src/storage/
|
|
111
|
+
// src/storage/yrestStorage.ts
|
|
112
112
|
import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
113
113
|
import { resolve as resolve2, dirname } from "path";
|
|
114
114
|
import { randomUUID } from "crypto";
|
|
@@ -121,8 +121,8 @@ function deepCopyData(source) {
|
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// src/storage/
|
|
125
|
-
function
|
|
124
|
+
// src/storage/yrestStorage.ts
|
|
125
|
+
function createYrestStorage(filePath) {
|
|
126
126
|
const absPath = resolve2(filePath);
|
|
127
127
|
const raw = parse(readFileSync(absPath, "utf8")) ?? {};
|
|
128
128
|
const relations = raw["_rel"] ?? {};
|
|
@@ -442,16 +442,33 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
442
442
|
<table><tbody>
|
|
443
443
|
${customRoutes.map((r) => {
|
|
444
444
|
const fullPath = `${base}${r.path}`;
|
|
445
|
+
const tags = [];
|
|
446
|
+
if (r.delay && r.delay > 0) {
|
|
447
|
+
tags.push(`<span style="color:#fb923c;font-size:11px">delay\xB7${r.delay}ms</span>`);
|
|
448
|
+
}
|
|
449
|
+
if (r.scenarios?.length) {
|
|
450
|
+
const hasOr = r.scenarios.some((s) => Array.isArray(s.when));
|
|
451
|
+
tags.push(
|
|
452
|
+
`<span style="color:#a371f7;font-size:11px">scenarios\xB7${r.scenarios.length}${hasOr ? " (OR)" : ""}</span>`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (r.otherwise) {
|
|
456
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">otherwise</span>`);
|
|
457
|
+
}
|
|
445
458
|
let desc;
|
|
446
459
|
if (r.handler) {
|
|
447
460
|
const found = handlers.has(r.handler);
|
|
448
461
|
desc = found ? `Handler \u2014 <code>${r.handler}()</code>` : `Handler \u2014 <code>${r.handler}()</code> <span style="color:#f85149">(not loaded)</span>`;
|
|
462
|
+
} else if (r.scenarios?.length) {
|
|
463
|
+
const hasTemplateInScenarios = r.scenarios.some((s) => s.response.body != null && hasTemplates(s.response.body)) || r.otherwise?.body != null && hasTemplates(r.otherwise.body);
|
|
464
|
+
desc = hasTemplateInScenarios ? `Scenarios \u2014 <code>{{\u2026}}</code>` : `Scenarios`;
|
|
449
465
|
} else if (r.response?.body != null && hasTemplates(r.response.body)) {
|
|
450
|
-
desc = `Dynamic
|
|
466
|
+
desc = `Dynamic \u2014 <code>{{\u2026}}</code>`;
|
|
451
467
|
} else {
|
|
452
468
|
const status = r.response?.status ?? 200;
|
|
453
|
-
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` +
|
|
469
|
+
desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
|
|
454
470
|
}
|
|
471
|
+
if (tags.length) desc += ` ${tags.join(" ")}`;
|
|
455
472
|
return endpointRow(r.method?.toUpperCase() ?? "GET", fullPath, desc);
|
|
456
473
|
}).join("")}
|
|
457
474
|
</tbody></table>
|
|
@@ -603,7 +620,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
603
620
|
|
|
604
621
|
<div class="banner">
|
|
605
622
|
<div class="banner-inner">
|
|
606
|
-
<h1><span class="y">y</span><span class="rest">
|
|
623
|
+
<h1><span class="y">y</span><span class="rest">Rest</span></h1>
|
|
607
624
|
<p>Zero-config REST API mock server</p>
|
|
608
625
|
<div class="banner-meta">
|
|
609
626
|
<span>URL <strong>${host}</strong></span>
|
|
@@ -655,7 +672,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
655
672
|
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
656
673
|
|
|
657
674
|
<footer>
|
|
658
|
-
Powered by <a href="https://github.com/aggiovato/
|
|
675
|
+
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
659
676
|
</footer>
|
|
660
677
|
|
|
661
678
|
</div>
|
|
@@ -960,7 +977,63 @@ var CollectionRouteCommand = class {
|
|
|
960
977
|
}
|
|
961
978
|
};
|
|
962
979
|
|
|
980
|
+
// src/utils/conditions.ts
|
|
981
|
+
function resolveRequestPath(dotPath, req) {
|
|
982
|
+
const [root, ...rest] = dotPath.split(".");
|
|
983
|
+
let value;
|
|
984
|
+
switch (root) {
|
|
985
|
+
case "body":
|
|
986
|
+
value = req.body;
|
|
987
|
+
break;
|
|
988
|
+
case "params":
|
|
989
|
+
value = req.params;
|
|
990
|
+
break;
|
|
991
|
+
case "query":
|
|
992
|
+
value = req.query;
|
|
993
|
+
break;
|
|
994
|
+
case "headers":
|
|
995
|
+
value = req.headers;
|
|
996
|
+
break;
|
|
997
|
+
default:
|
|
998
|
+
return void 0;
|
|
999
|
+
}
|
|
1000
|
+
for (const key of rest) {
|
|
1001
|
+
if (value != null && typeof value === "object") {
|
|
1002
|
+
value = value[key];
|
|
1003
|
+
} else {
|
|
1004
|
+
return void 0;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return value;
|
|
1008
|
+
}
|
|
1009
|
+
function matchConditionGroup(group, req) {
|
|
1010
|
+
return Object.entries(group).every(([key, expected]) => {
|
|
1011
|
+
const op = OPERATORS.find((o) => key.endsWith(o));
|
|
1012
|
+
if (op) {
|
|
1013
|
+
const path = key.slice(0, -op.length);
|
|
1014
|
+
const value2 = resolveRequestPath(path, req);
|
|
1015
|
+
if (value2 === void 0) return false;
|
|
1016
|
+
return applyOperator(value2, op, String(expected));
|
|
1017
|
+
}
|
|
1018
|
+
const value = resolveRequestPath(key, req);
|
|
1019
|
+
return String(value) === String(expected);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function matchWhen(when, req) {
|
|
1023
|
+
if (Array.isArray(when)) {
|
|
1024
|
+
return when.some((group) => matchConditionGroup(group, req));
|
|
1025
|
+
}
|
|
1026
|
+
return matchConditionGroup(when, req);
|
|
1027
|
+
}
|
|
1028
|
+
function findMatchingScenario(scenarios, req) {
|
|
1029
|
+
return scenarios.find((s) => matchWhen(s.when, req));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
963
1032
|
// src/router/routes/custom.routes.ts
|
|
1033
|
+
function resolveBody(body, ctx) {
|
|
1034
|
+
if (body != null && hasTemplates(body)) return interpolate(body, ctx);
|
|
1035
|
+
return body ?? null;
|
|
1036
|
+
}
|
|
964
1037
|
var CustomRouteCommand = class {
|
|
965
1038
|
constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
|
|
966
1039
|
this.storage = storage;
|
|
@@ -985,6 +1058,9 @@ var CustomRouteCommand = class {
|
|
|
985
1058
|
method,
|
|
986
1059
|
url,
|
|
987
1060
|
handler: async (req, reply) => {
|
|
1061
|
+
if (route.delay && route.delay > 0) {
|
|
1062
|
+
await new Promise((resolve5) => setTimeout(resolve5, route.delay));
|
|
1063
|
+
}
|
|
988
1064
|
for (const [key, value] of Object.entries(headers)) {
|
|
989
1065
|
reply.header(key, value);
|
|
990
1066
|
}
|
|
@@ -994,13 +1070,13 @@ var CustomRouteCommand = class {
|
|
|
994
1070
|
return reply.status(501).send({ error: `Handler "${handlerName}" is not defined in the handlers file` });
|
|
995
1071
|
}
|
|
996
1072
|
try {
|
|
997
|
-
const
|
|
1073
|
+
const ctx2 = {
|
|
998
1074
|
params: req.params,
|
|
999
1075
|
query: req.query,
|
|
1000
1076
|
body: req.body,
|
|
1001
1077
|
headers: req.headers
|
|
1002
1078
|
};
|
|
1003
|
-
const result = await fn(
|
|
1079
|
+
const result = await fn(ctx2);
|
|
1004
1080
|
const resStatus = result.status ?? 200;
|
|
1005
1081
|
for (const [k, v] of Object.entries(result.headers ?? {})) {
|
|
1006
1082
|
reply.header(k, v);
|
|
@@ -1012,12 +1088,24 @@ var CustomRouteCommand = class {
|
|
|
1012
1088
|
return reply.status(500).send({ error: `Handler "${handlerName}" threw an error: ${msg}` });
|
|
1013
1089
|
}
|
|
1014
1090
|
}
|
|
1015
|
-
const
|
|
1091
|
+
const ctx = {
|
|
1016
1092
|
params: req.params,
|
|
1017
1093
|
query: req.query,
|
|
1018
1094
|
body: req.body,
|
|
1019
1095
|
headers: req.headers
|
|
1020
|
-
}
|
|
1096
|
+
};
|
|
1097
|
+
if (route.scenarios?.length) {
|
|
1098
|
+
const matched = findMatchingScenario(route.scenarios, ctx);
|
|
1099
|
+
const active = matched?.response ?? route.otherwise;
|
|
1100
|
+
if (active) {
|
|
1101
|
+
const aStatus = active.status ?? 200;
|
|
1102
|
+
const aBody = resolveBody(active.body, ctx);
|
|
1103
|
+
for (const [k, v] of Object.entries(active.headers ?? {})) reply.header(k, v);
|
|
1104
|
+
if (!active.body && aStatus === 204) return reply.status(aStatus).send();
|
|
1105
|
+
return reply.status(aStatus).send(aBody);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const body = dynamic ? interpolate(rawBody, ctx) : rawBody;
|
|
1021
1109
|
if (body === null && status === 204) return reply.status(status).send();
|
|
1022
1110
|
return reply.status(status).send(body);
|
|
1023
1111
|
}
|
|
@@ -1204,9 +1292,37 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
|
|
|
1204
1292
|
return server;
|
|
1205
1293
|
}
|
|
1206
1294
|
|
|
1295
|
+
// src/server/yrestServer.ts
|
|
1296
|
+
function createYrestServerFromStorage(storage, options, handlers = /* @__PURE__ */ new Map()) {
|
|
1297
|
+
let _port = 0;
|
|
1298
|
+
let _started = false;
|
|
1299
|
+
let _fastify = null;
|
|
1300
|
+
return {
|
|
1301
|
+
async start() {
|
|
1302
|
+
if (_started) return;
|
|
1303
|
+
_fastify = await createServer(storage, options, handlers);
|
|
1304
|
+
await _fastify.listen({ port: options.port, host: options.host });
|
|
1305
|
+
const address = _fastify.addresses()[0];
|
|
1306
|
+
_port = typeof address === "object" && "port" in address ? address.port : options.port;
|
|
1307
|
+
_started = true;
|
|
1308
|
+
},
|
|
1309
|
+
async stop() {
|
|
1310
|
+
if (!_started || !_fastify) return;
|
|
1311
|
+
await _fastify.close();
|
|
1312
|
+
_started = false;
|
|
1313
|
+
},
|
|
1314
|
+
get port() {
|
|
1315
|
+
return _port;
|
|
1316
|
+
},
|
|
1317
|
+
get url() {
|
|
1318
|
+
return `http://${options.host}:${_port}${options.base}`;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1207
1323
|
// src/config/loadOptions.ts
|
|
1208
1324
|
import { z } from "zod";
|
|
1209
|
-
var
|
|
1325
|
+
var yrestOptionsSchema = z.object({
|
|
1210
1326
|
/** Path to the YAML database file. Must be a non-empty string. */
|
|
1211
1327
|
file: z.string().min(1),
|
|
1212
1328
|
/** TCP port the server listens on. Accepts string input and coerces to number. */
|
|
@@ -1297,18 +1413,18 @@ function registerServe(program2) {
|
|
|
1297
1413
|
...cmd.args.length > 0 ? { file } : {},
|
|
1298
1414
|
...cliOverrides
|
|
1299
1415
|
};
|
|
1300
|
-
const options =
|
|
1416
|
+
const options = yrestOptionsSchema.parse(merged);
|
|
1301
1417
|
let storage;
|
|
1302
1418
|
try {
|
|
1303
|
-
storage =
|
|
1419
|
+
storage = createYrestStorage(options.file);
|
|
1304
1420
|
} catch (err) {
|
|
1305
1421
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1306
1422
|
console.error(`Error: cannot load "${options.file}" \u2014 ${msg}`);
|
|
1307
1423
|
process.exit(1);
|
|
1308
1424
|
}
|
|
1309
1425
|
const handlers = options.handlers ? await loadHandlers(resolve3(options.handlers)) : /* @__PURE__ */ new Map();
|
|
1310
|
-
const
|
|
1311
|
-
await
|
|
1426
|
+
const yrestServer = createYrestServerFromStorage(storage, options, handlers);
|
|
1427
|
+
await yrestServer.start();
|
|
1312
1428
|
const collections = Object.keys(storage.getData());
|
|
1313
1429
|
const customRoutes = storage.getRoutes();
|
|
1314
1430
|
const base = options.base || "/";
|
|
@@ -1328,7 +1444,7 @@ function registerServe(program2) {
|
|
|
1328
1444
|
};
|
|
1329
1445
|
console.log(
|
|
1330
1446
|
`
|
|
1331
|
-
${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${
|
|
1447
|
+
${b("yrest")} ${dim("\xB7")} ${green(`http://${options.host}:${yrestServer.port}`)}
|
|
1332
1448
|
`
|
|
1333
1449
|
);
|
|
1334
1450
|
console.log(` ${b("Collections")} ${dim(`(base: ${base})`)}:`);
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import * as fastify from 'fastify';
|
|
2
3
|
import * as http from 'http';
|
|
3
|
-
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
/** A single REST resource item. Field names and value types are user-defined in the YAML file. */
|
|
6
6
|
type Resource = Record<string, unknown>;
|
|
@@ -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,23 @@ 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;
|
|
58
115
|
};
|
|
59
116
|
/**
|
|
60
117
|
* In-memory store backed by a YAML file.
|
|
@@ -63,7 +120,7 @@ type CustomRoute = {
|
|
|
63
120
|
* flushed to disk by calling {@link persist}. Use {@link reload} to pull in
|
|
64
121
|
* changes made to the file externally (e.g. in watch mode).
|
|
65
122
|
*/
|
|
66
|
-
interface
|
|
123
|
+
interface YrestStorage {
|
|
67
124
|
/** Returns the full in-memory dataset (all collections). */
|
|
68
125
|
getData(): Data;
|
|
69
126
|
/** Returns the relational mappings declared under `_rel`. */
|
|
@@ -130,16 +187,27 @@ interface YamlStorage {
|
|
|
130
187
|
}
|
|
131
188
|
|
|
132
189
|
/**
|
|
133
|
-
*
|
|
190
|
+
* Tagged template literal that parses inline YAML into a yRest {@link Data} object.
|
|
134
191
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* all other top-level keys become collections.
|
|
192
|
+
* Strips common leading indentation automatically, so the template can be
|
|
193
|
+
* indented naturally inside the calling function without affecting YAML parsing.
|
|
138
194
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
195
|
+
* Supports interpolated values — they are stringified and inserted inline.
|
|
196
|
+
*
|
|
197
|
+
* @throws {Error} If the template is not valid YAML or does not resolve to a plain object.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* const data = yrest`
|
|
201
|
+
* users:
|
|
202
|
+
* - id: 1
|
|
203
|
+
* name: Ana
|
|
204
|
+
* posts:
|
|
205
|
+
* - id: 1
|
|
206
|
+
* title: First post
|
|
207
|
+
* userId: 1
|
|
208
|
+
* `;
|
|
141
209
|
*/
|
|
142
|
-
declare function
|
|
210
|
+
declare function yrest(strings: TemplateStringsArray, ...values: unknown[]): Data;
|
|
143
211
|
|
|
144
212
|
/**
|
|
145
213
|
* Zod schema for all server runtime options.
|
|
@@ -147,7 +215,7 @@ declare function createYamlStorage(filePath: string): YamlStorage;
|
|
|
147
215
|
* Validates and normalises options from three sources in ascending priority:
|
|
148
216
|
* schema defaults → `yrest.config.yml` → explicit CLI flags.
|
|
149
217
|
*/
|
|
150
|
-
declare const
|
|
218
|
+
declare const yrestOptionsSchema: z.ZodObject<{
|
|
151
219
|
/** Path to the YAML database file. Must be a non-empty string. */
|
|
152
220
|
file: z.ZodString;
|
|
153
221
|
/** TCP port the server listens on. Accepts string input and coerces to number. */
|
|
@@ -211,9 +279,9 @@ declare const serverOptionsSchema: z.ZodObject<{
|
|
|
211
279
|
}>;
|
|
212
280
|
/**
|
|
213
281
|
* Resolved server configuration after Zod validation and transformation.
|
|
214
|
-
* Inferred from {@link
|
|
282
|
+
* Inferred from {@link yrestOptionsSchema}.
|
|
215
283
|
*/
|
|
216
|
-
type
|
|
284
|
+
type YrestOptions = z.infer<typeof yrestOptionsSchema>;
|
|
217
285
|
|
|
218
286
|
/** Incoming request data passed to every handler function. */
|
|
219
287
|
type HandlerRequest = {
|
|
@@ -252,6 +320,113 @@ type HandlerMap = Map<string, Handler>;
|
|
|
252
320
|
* @param options - Validated server options.
|
|
253
321
|
* @param handlers - Map of named handler functions loaded from yrest.handlers.js.
|
|
254
322
|
*/
|
|
255
|
-
declare function createServer(storage:
|
|
323
|
+
declare function createServer(storage: YrestStorage, options: YrestOptions, handlers?: HandlerMap): Promise<fastify.FastifyInstance<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault>>;
|
|
324
|
+
|
|
325
|
+
/** Handle returned by {@link createYrestServerFromStorage} and {@link createYrestServer}. */
|
|
326
|
+
interface YrestServer {
|
|
327
|
+
/** Starts the server and begins listening on the configured port. */
|
|
328
|
+
start(): Promise<void>;
|
|
329
|
+
/** Gracefully closes the server. */
|
|
330
|
+
stop(): Promise<void>;
|
|
331
|
+
/** The port the server is listening on. Only valid after `start()`. */
|
|
332
|
+
readonly port: number;
|
|
333
|
+
/** The base URL of the server (e.g. `http://localhost:3070`). Only valid after `start()`. */
|
|
334
|
+
readonly url: string;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Creates a {@link YrestServer} from an already-initialised storage and parsed options.
|
|
338
|
+
*
|
|
339
|
+
* This is the shared Fastify lifecycle owner used by both the CLI (`serve` command)
|
|
340
|
+
* and the programmatic API (`createYrestServer`). It is the only place where
|
|
341
|
+
* `createServer → listen → close` lives.
|
|
342
|
+
*
|
|
343
|
+
* Each consumer is responsible for building storage and resolving options before
|
|
344
|
+
* calling this function:
|
|
345
|
+
* - CLI (`serve.ts`): Zod parsing, config-file merging, `process.exit` error handling
|
|
346
|
+
* - Programmatic API (`createYrestServer`): raw → parsed options conversion, inline data support
|
|
347
|
+
*
|
|
348
|
+
* @param storage - Initialised storage instance (file-based or in-memory).
|
|
349
|
+
* @param options - Fully resolved and validated server options.
|
|
350
|
+
* @param handlers - Pre-loaded handler map. Defaults to an empty map.
|
|
351
|
+
*/
|
|
352
|
+
declare function createYrestServerFromStorage(storage: YrestStorage, options: YrestOptions, handlers?: HandlerMap): YrestServer;
|
|
353
|
+
|
|
354
|
+
/** Base options shared between file-based and data-based server instances. */
|
|
355
|
+
type YrestServerBaseOptions = {
|
|
356
|
+
/** TCP port to listen on. Defaults to `3070`. Use `0` to get a random available port. */
|
|
357
|
+
port?: number;
|
|
358
|
+
/** Host to bind. Defaults to `"localhost"`. */
|
|
359
|
+
host?: string;
|
|
360
|
+
/** URL prefix prepended to every route (e.g. `"/api"`). */
|
|
361
|
+
base?: string;
|
|
362
|
+
/** Reject all mutating requests (POST, PUT, PATCH, DELETE) with `405`. */
|
|
363
|
+
readonly?: boolean;
|
|
364
|
+
/** Milliseconds to delay every response. */
|
|
365
|
+
delay?: number;
|
|
366
|
+
/** Wrap GET collection responses in `{ data, pagination }`. Pass `true` (limit 10) or a number. */
|
|
367
|
+
pageable?: boolean | number;
|
|
368
|
+
/** Save a snapshot at startup and expose `/_snapshot` endpoints. */
|
|
369
|
+
snapshot?: boolean;
|
|
370
|
+
/** Path to a JS file exporting handler functions for custom `_routes` entries. */
|
|
371
|
+
handlers?: string;
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Options for {@link createYrestServer}.
|
|
375
|
+
* Either `file` (path to a `db.yml`) or `data` (inline object, e.g. from `yrest\`...\``) is required.
|
|
376
|
+
*/
|
|
377
|
+
type YrestServerOptions = YrestServerBaseOptions & ({
|
|
378
|
+
file: string;
|
|
379
|
+
data?: never;
|
|
380
|
+
} | {
|
|
381
|
+
data: Data;
|
|
382
|
+
file?: never;
|
|
383
|
+
});
|
|
384
|
+
/**
|
|
385
|
+
* Creates a yRest server instance for programmatic use.
|
|
386
|
+
*
|
|
387
|
+
* Accepts either a `file` path to a `db.yml` or an inline `data` object
|
|
388
|
+
* (typically produced by the {@link yrest} tagged template literal).
|
|
389
|
+
*
|
|
390
|
+
* The server is not started until you call `start()`.
|
|
391
|
+
*
|
|
392
|
+
* @example — file-based (e.g. in Playwright globalSetup)
|
|
393
|
+
* ```ts
|
|
394
|
+
* const server = createYrestServer({ file: "./tests/db.yml", readonly: true });
|
|
395
|
+
* await server.start();
|
|
396
|
+
* // → http://localhost:3070
|
|
397
|
+
* await server.stop();
|
|
398
|
+
* ```
|
|
399
|
+
*
|
|
400
|
+
* @example — inline data (e.g. in Vitest)
|
|
401
|
+
* ```ts
|
|
402
|
+
* import { createYrestServer, yrest } from "@yrest/cli";
|
|
403
|
+
*
|
|
404
|
+
* const server = createYrestServer({
|
|
405
|
+
* data: yrest`
|
|
406
|
+
* users:
|
|
407
|
+
* - id: 1
|
|
408
|
+
* name: Ana
|
|
409
|
+
* `,
|
|
410
|
+
* port: 0,
|
|
411
|
+
* readonly: true,
|
|
412
|
+
* });
|
|
413
|
+
*
|
|
414
|
+
* beforeAll(() => server.start());
|
|
415
|
+
* afterAll(() => server.stop());
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
declare function createYrestServer(options: YrestServerOptions): YrestServer;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Creates a {@link YrestStorage} instance backed by the given YAML file.
|
|
422
|
+
*
|
|
423
|
+
* The file is read and parsed eagerly on construction. The `_rel` key is
|
|
424
|
+
* extracted as relational metadata; `_routes` as custom route declarations;
|
|
425
|
+
* all other top-level keys become collections.
|
|
426
|
+
*
|
|
427
|
+
* @param filePath - Relative or absolute path to the YAML database file.
|
|
428
|
+
* @throws {Error} If the file cannot be read or its YAML is invalid.
|
|
429
|
+
*/
|
|
430
|
+
declare function createYrestStorage(filePath: string): YrestStorage;
|
|
256
431
|
|
|
257
|
-
export { type CustomRoute, type Data, type Handler, type HandlerMap, type HandlerRequest, type HandlerResponse, type Relations, type Resource, type
|
|
432
|
+
export { type CustomRoute, type Data, type Handler, type HandlerMap, type HandlerRequest, type HandlerResponse, type Relations, type Resource, type YrestOptions, type YrestServer, type YrestServerBaseOptions, type YrestServerOptions, type YrestStorage, createServer, createYrestServer, createYrestServerFromStorage, createYrestStorage, yrest, yrestOptionsSchema };
|