@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/dist/index.d.ts 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 };