@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.
@@ -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/yamlStorage.ts
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/yamlStorage.ts
125
- function createYamlStorage(filePath) {
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 body \u2014 <code>{{\u2026}}</code>`;
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 ? ` + custom headers` : ""}`;
469
+ desc = `Static \u2014 <code>${status}</code>${r.response?.headers ? ` + headers` : ""}`;
454
470
  }
471
+ if (tags.length) desc += `&ensp;${tags.join("&ensp;")}`;
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">rest</span></h1>
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/yaml-rest" target="_blank">@aggiovato/yrest</a> &nbsp;\xB7&nbsp; <a href="/_about">/_about</a>
675
+ Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> &nbsp;\xB7&nbsp; <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 ctx = {
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(ctx);
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 body = dynamic ? interpolate(rawBody, {
1091
+ const ctx = {
1016
1092
  params: req.params,
1017
1093
  query: req.query,
1018
1094
  body: req.body,
1019
1095
  headers: req.headers
1020
- }) : rawBody;
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 serverOptionsSchema = z.object({
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 = serverOptionsSchema.parse(merged);
1416
+ const options = yrestOptionsSchema.parse(merged);
1301
1417
  let storage;
1302
1418
  try {
1303
- storage = createYamlStorage(options.file);
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 server = await createServer(storage, options, handlers);
1311
- await server.listen({ port: options.port, host: options.host });
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}:${options.port}`)}
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
- * Custom routes are registered before resource routes and take priority over them.
28
- * They always return a static, pre-defined response regardless of the request body or params.
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
- * // response:
35
- * // status: 200
36
- * // body: { token: abc123 }
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
- * When set, the function is called on every request and its return value is used as the response.
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
- /** Static or template response. Used when `handler` is absent or not found in the handlers file. */
50
- response?: {
51
- /** HTTP status code. Defaults to `200` if omitted. */
52
- status?: number;
53
- /** Response body. Any YAML-serialisable value (object, array, string, number, null). */
54
- body?: unknown;
55
- /** Additional response headers to set alongside `Content-Type`. */
56
- headers?: Record<string, string>;
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 YamlStorage {
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
- * Creates a {@link YamlStorage} instance backed by the given YAML file.
190
+ * Tagged template literal that parses inline YAML into a yRest {@link Data} object.
134
191
  *
135
- * The file is read and parsed eagerly on construction. The `_rel` key is
136
- * extracted as relational metadata; `_routes` as custom route declarations;
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
- * @param filePath - Relative or absolute path to the YAML database file.
140
- * @throws {Error} If the file cannot be read or its YAML is invalid.
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 createYamlStorage(filePath: string): YamlStorage;
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 serverOptionsSchema: z.ZodObject<{
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 serverOptionsSchema}.
282
+ * Inferred from {@link yrestOptionsSchema}.
215
283
  */
216
- type ServerOptions = z.infer<typeof serverOptionsSchema>;
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: YamlStorage, options: ServerOptions, handlers?: HandlerMap): Promise<fastify.FastifyInstance<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, fastify.FastifyBaseLogger, fastify.FastifyTypeProviderDefault>>;
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 ServerOptions, type YamlStorage, createServer, createYamlStorage, serverOptionsSchema };
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 };