@yrest/cli 0.13.0 → 0.14.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 CHANGED
@@ -8,14 +8,17 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dw/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
9
9
  [![license](https://img.shields.io/npm/l/@yrest/cli)](LICENSE)
10
10
  [![CI](https://github.com/aggiovato/yRest/actions/workflows/ci.yml/badge.svg)](https://github.com/aggiovato/yRest/actions)
11
+ [![Netlify Status](https://api.netlify.com/api/v1/badges/cca9b352-b564-4027-a05d-af26728c8ac3/deploy-status)](https://app.netlify.com/projects/cca9b352-b564-4027-a05d-af26728c8ac3/deploys)
11
12
  [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
12
13
  [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
13
- [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.13.0)](https://socket.dev/npm/package/@yrest/cli)
14
+ [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.14.0)](https://socket.dev/npm/package/@yrest/cli)
14
15
 
15
16
  YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
16
17
 
17
18
  > Think `json-server`, but powered by YAML — with relations, filters, pagination, nested routes, snapshots and custom handlers.
18
19
 
20
+ **[📖 Full documentation](https://yrest-docs.netlify.app)** — Getting Started, CLI reference, YAML format, relations, custom routes, SSE streams, playground and more.
21
+
19
22
  ```yaml
20
23
  # db.yml
21
24
  users:
@@ -62,7 +65,7 @@ A YAML-first alternative to json-server for frontend development.
62
65
  | Pageable mode (envelope response) | ✅ | ❌ |
63
66
  | Custom static routes (`_routes`) | ✅ | ❌ |
64
67
  | Template variables in responses | ✅ | ❌ |
65
- | Handler functions (JS logic) | ✅ | ❌ |
68
+ | Handler functions (JS / TS logic) | ✅ | ❌ |
66
69
  | Conditional scenarios (`scenarios:`) | ✅ | ❌ |
67
70
  | Snapshot endpoints | ✅ | ❌ |
68
71
  | Config file | ✅ | ⚠️ |
@@ -70,7 +73,7 @@ A YAML-first alternative to json-server for frontend development.
70
73
  | Watch mode | ✅ | ✅ |
71
74
  | Readonly mode | ✅ | ❌ |
72
75
  | Atomic writes | ✅ | ✅ |
73
- | TypeScript types | ✅ | ❌ |
76
+ | TypeScript types + codegen (`yrest types`) | ✅ | ❌ |
74
77
  | Programmatic API for test frameworks | ✅ | ❌ |
75
78
  | OpenAPI 3.0 spec (`GET /_openapi`, `yrest openapi`) | ✅ | ❌ |
76
79
  | Field annotations (`_schema`) | ✅ | ❌ |
@@ -169,6 +172,104 @@ The spec is also available live at `GET /_openapi` (YAML) and `GET /_openapi.jso
169
172
 
170
173
  ---
171
174
 
175
+ ### `types`
176
+
177
+ Generates TypeScript interfaces from a `db.yml` file — one per collection, plus a `CollectionName` union and a `Collections` map.
178
+
179
+ ```bash
180
+ npx @yrest/cli types db.yml # prints to stdout
181
+ npx @yrest/cli types db.yml --output db.types.ts # writes a file
182
+ npx @yrest/cli types db.yml --output db.types.ts --watch # regenerates on every change
183
+ ```
184
+
185
+ | Flag | Default | Description |
186
+ | ----------------- | ---------- | --------------------------------------------------------- |
187
+ | `--output <file>` | _(stdout)_ | Write to a file instead of printing to stdout |
188
+ | `--watch` | `false` | Regenerate on every change of the file (needs `--output`) |
189
+
190
+ Given this `db.yml`:
191
+
192
+ ```yaml
193
+ _schema:
194
+ users:
195
+ email:
196
+ _required: true
197
+ _format: email
198
+ role:
199
+ _enum: [admin, editor]
200
+ _rel:
201
+ posts:
202
+ userId: users
203
+ users:
204
+ - id: 1
205
+ name: Ana
206
+ email: ana@test.com
207
+ role: admin
208
+ - id: 2
209
+ name: Luis
210
+ address: { city: Lima }
211
+ posts:
212
+ - id: 1
213
+ title: Hello
214
+ userId: 1
215
+ ```
216
+
217
+ `yrest types db.yml` prints:
218
+
219
+ ```ts
220
+ // Auto-generated by yrest types — do not edit manually.
221
+ // Source: db.yml
222
+
223
+ /** Item of the `users` collection. */
224
+ export interface User {
225
+ id: number;
226
+ name: string;
227
+ /**
228
+ * @format email
229
+ */
230
+ email: string;
231
+ role?: "admin" | "editor";
232
+ address?: { city: string };
233
+ }
234
+
235
+ /** Item of the `posts` collection. */
236
+ export interface Post {
237
+ id: number;
238
+ title: string;
239
+ /** FK → users.id */
240
+ userId: number;
241
+ }
242
+
243
+ /** Union of all collection names in the source file. */
244
+ export type CollectionName = "users" | "posts";
245
+
246
+ /** Maps each collection name to its item array type. */
247
+ export interface Collections {
248
+ users: User[];
249
+ posts: Post[];
250
+ }
251
+ ```
252
+
253
+ **How types are derived:**
254
+
255
+ - Interface names are the collection name in singular PascalCase (`users` → `User`, `post_tags` → `PostTag`, `categories` → `Category`). If two names collide, the plural form is kept.
256
+ - Types are inferred from **every** item. A field present on all items is required; a field missing on any item is optional (`?`).
257
+ - Different types across items become a union (`number | string`); `null` values add `| null`. Nested objects are merged into one inline shape and array elements are pooled (`{ k?: string; v?: number }[]`).
258
+ - `_schema` overrides the inference: `_type` (`integer` → `number`), `_required` (or the `required` / `optional` shorthand), `_enum` (literal union), `_nullable` and `_items`. `_description`, `_format` and `_deprecated` are emitted as JSDoc. Fields declared only in `_schema` are included.
259
+ - `many2one` / `one2one` foreign keys from `_rel` are annotated with `FK → target.id`.
260
+
261
+ The command only reads the file: `__uuid_gen` / `__fk` directives are **not** resolved and `db.yml` is never rewritten (unresolved directive values are typed as `string`).
262
+
263
+ The generator is also available programmatically:
264
+
265
+ ```ts
266
+ import { generateTypes } from "@yrest/cli";
267
+
268
+ const source = generateTypes({ data: { users: [{ id: 1, name: "Ana" }] } });
269
+ ```
270
+
271
+ ---
272
+
172
273
  ### `serve`
173
274
 
174
275
  Starts the mock server.
@@ -193,7 +294,7 @@ npx @yrest/cli serve db.yml --handlers yrest.handlers.js
193
294
  | `--delay <ms>` | `0` | Add a fixed delay to all responses (simulates network latency) |
194
295
  | `--pageable [n]` | `false` | Wrap GET collection responses in `{ data, pagination }`. Optional limit |
195
296
  | `--snapshot` | `false` | Save initial state snapshot and expose `/_snapshot` endpoints |
196
- | `--handlers <file>` | _(none)_ | Path to a JS file exporting handler functions for custom routes |
297
+ | `--handlers <file>` | _(none)_ | Path to a JS or TS file exporting handler functions for custom routes |
197
298
  | `--id-strategy` | `increment` | Id generation for new items: `increment` (auto-int) or `uuid` |
198
299
 
199
300
  All flags can also be set in `yrest.config.yml` (see below). CLI flags always take priority over the config file.
@@ -903,41 +1004,57 @@ _routes:
903
1004
 
904
1005
  ### Handler functions
905
1006
 
906
- For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the `handler:` field:
1007
+ For routes that need real logic (conditional responses, stateful mocks, request inspection), point a `_routes` entry at a function with `_handler`:
907
1008
 
908
1009
  ```yaml
909
1010
  _routes:
910
- - method: POST
911
- path: /login
912
- handler: login
913
- response: # optional fallback if handler throws
914
- status: 200
915
- body: { token: fake }
1011
+ - _method: POST
1012
+ _path: /login
1013
+ _handler: login
916
1014
 
917
- - method: GET
918
- path: /auth/me
919
- handler: getCurrentUser
1015
+ - _method: GET
1016
+ _path: /auth/me
1017
+ _handler: getCurrentUser
920
1018
  ```
921
1019
 
922
- Create a `yrest.handlers.js` file in the same directory as `db.yml`:
1020
+ Export the functions from a handlers file — JavaScript (`.js`, `.mjs`, `.cjs`) or TypeScript (`.ts`, `.mts`):
923
1021
 
924
- ```js
925
- // yrest.handlers.js
926
- export async function login(req) {
927
- const { email, password } = req.body ?? {};
928
- if (password !== "secret") return { status: 401, body: { error: "Invalid credentials" } };
929
- return { status: 200, body: { token: `tok-${email}` } };
930
- }
1022
+ ```ts
1023
+ // yrest.handlers.ts
1024
+ import { defineHandler } from "@yrest/cli";
1025
+ import type { User } from "./db.types"; // generated with `yrest types`
1026
+
1027
+ export const login = defineHandler((req, { collection }) => {
1028
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
1029
+ const user = collection<User>("users").findWhere((u) => u.email === email)[0];
1030
+ if (!user || password !== "secret") {
1031
+ return { status: 401, body: { error: "Invalid credentials" } };
1032
+ }
1033
+ return { body: { token: `tok-${user.id}` } };
1034
+ });
931
1035
 
932
- export async function getCurrentUser(req) {
933
- return { status: 200, body: { id: 1, name: "Ana", role: "admin" } };
934
- }
1036
+ export const getCurrentUser = defineHandler(() => ({
1037
+ body: { id: 1, name: "Ana", role: "admin" },
1038
+ }));
935
1039
  ```
936
1040
 
937
- Pass the file to the server with `--handlers`:
1041
+ Pass the file to the server with `--handlers` (or `handlers:` in `yrest.config.yml`):
938
1042
 
939
1043
  ```bash
940
- npx @yrest/cli serve db.yml --handlers yrest.handlers.js
1044
+ npx @yrest/cli serve db.yml --handlers yrest.handlers.ts
1045
+ ```
1046
+
1047
+ TypeScript handler files are loaded by Node's built-in type stripping (Node ≥ 22.18). On older versions load [`tsx`](https://tsx.is) into Node (`NODE_OPTIONS="--import tsx" npx @yrest/cli serve …`) or compile the file to `.js`. With type stripping, type-only imports must use `import type`.
1048
+
1049
+ In plain JavaScript the same functions work without `defineHandler`:
1050
+
1051
+ ```js
1052
+ // yrest.handlers.js
1053
+ export async function login(req, ctx) {
1054
+ const { email } = req.body ?? {};
1055
+ const user = ctx.collection("users").findWhere((u) => u.email === email)[0];
1056
+ return user ? { body: { token: `tok-${user.id}` } } : { status: 401 };
1057
+ }
941
1058
  ```
942
1059
 
943
1060
  **Handler signature:**
@@ -956,10 +1073,24 @@ type HandlerResponse = {
956
1073
  headers?: Record<string, string>;
957
1074
  };
958
1075
 
959
- type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>;
1076
+ type HandlerContext = {
1077
+ // Typed CRUD access to the live data — the same handle as server.collection()
1078
+ collection<T>(name: string): CollectionHandle<T>;
1079
+ };
1080
+
1081
+ type Handler = (
1082
+ req: HandlerRequest,
1083
+ ctx: HandlerContext
1084
+ ) => HandlerResponse | Promise<HandlerResponse>;
960
1085
  ```
961
1086
 
962
- If a named handler is not found in the file, the server returns `501`. If the handler throws, it returns `500`. If a `response:` block is defined alongside `handler:`, it is used as fallback only when the handler itself throws.
1087
+ `defineHandler` (and `defineSseHandler` for [SSE streams](#sse-streams)) are identity wrappers exported by `@yrest/cli`: they only add types.
1088
+
1089
+ **`ctx.collection(name)`** reads and writes the same data the REST routes serve: a record a handler creates is returned by `GET /users` straight away and persisted to `db.yml` like a `POST`. See [Typed collection access](#typed-collection-access) for every method. It throws if the collection does not exist.
1090
+
1091
+ **Errors:** if a named handler is not exported by the file, the route answers `501`; if the handler throws, it answers `500` with the error message.
1092
+
1093
+ **Scaffolding:** `yrest handler <name>` adds a stub to the handlers file (`handlers:` in `yrest.config.yml`, or `yrest.handlers.js`). For a `.ts` / `.mts` file the stub uses `defineHandler`. Add `--register --method POST --path /login` to also write the `_routes` entry to `db.yml`.
963
1094
 
964
1095
  ### SSE streams
965
1096
 
@@ -1009,6 +1140,41 @@ _events:
1009
1140
  filter: "{{query.filter}}" # from query string
1010
1141
  ```
1011
1142
 
1143
+ **Handler-driven streams** — for streams that depend on state (a chat, a job queue), let a function drive the stream with `_handler` instead of `_events`:
1144
+
1145
+ ```yaml
1146
+ _routes:
1147
+ - _method: SSE
1148
+ _path: /events/chat
1149
+ _sse:
1150
+ _handler: chatStream
1151
+ - _method: POST
1152
+ _path: /chat/messages
1153
+ _handler: postMessage
1154
+ ```
1155
+
1156
+ ```ts
1157
+ // yrest.handlers.ts
1158
+ import { EventEmitter } from "node:events";
1159
+ import { defineHandler, defineSseHandler } from "@yrest/cli";
1160
+
1161
+ const bus = new EventEmitter();
1162
+
1163
+ export const chatStream = defineSseHandler((req, raw) => {
1164
+ const send = (msg: unknown) => raw.write(`event: message\ndata: ${JSON.stringify(msg)}\n\n`);
1165
+ bus.on("message", send);
1166
+ raw.on("close", () => bus.off("message", send)); // client disconnected
1167
+ });
1168
+
1169
+ export const postMessage = defineHandler((req, { collection }) => {
1170
+ const message = collection("messages").create(req.body as { text: string });
1171
+ bus.emit("message", message);
1172
+ return { status: 201, body: message };
1173
+ });
1174
+ ```
1175
+
1176
+ The SSE handler receives `(req, raw, ctx)`, where `raw` is the Node.js `ServerResponse`. yRest sends the SSE headers and the keep-alive comments; the handler writes frames and calls `raw.end()` when it is done. A handler missing from the handlers file answers `501`; one that throws sends an `event: error` frame and the stream is closed.
1177
+
1012
1178
  **Consuming from the browser:**
1013
1179
 
1014
1180
  ```js
@@ -1207,16 +1373,47 @@ import { createYrestServer, yrest } from "@yrest/cli";
1207
1373
  | `delay` | `number` | `0` | Fixed delay in ms added to every response |
1208
1374
  | `pageable` | `boolean\|number` | `false` | Wrap GET responses in `{ data, pagination }` envelope |
1209
1375
  | `snapshot` | `boolean` | `false` | Enable snapshot endpoints (`/_snapshot`) |
1210
- | `handlers` | `string` | — | Path to a JS file exporting handler functions |
1376
+ | `handlers` | `string` | — | Path to a JS or TS file exporting handler functions |
1211
1377
 
1212
1378
  **Returned handle:**
1213
1379
 
1214
- | Member | Description |
1215
- | --------- | -------------------------------------------------------- |
1216
- | `start()` | Starts the server and begins listening |
1217
- | `stop()` | Gracefully closes the server |
1218
- | `port` | The actual port after `start()` (useful when `port: 0`) |
1219
- | `url` | Base URL after `start()` (e.g. `http://localhost:49821`) |
1380
+ | Member | Description |
1381
+ | --------------------- | ---------------------------------------------------------------- |
1382
+ | `start()` | Starts the server and begins listening |
1383
+ | `stop()` | Gracefully closes the server |
1384
+ | `port` | The actual port after `start()` (useful when `port: 0`) |
1385
+ | `url` | Base URL after `start()` (e.g. `http://localhost:49821`) |
1386
+ | `collection<T>(name)` | Typed CRUD handle for a collection — see below (after `start()`) |
1387
+
1388
+ ### Typed collection access
1389
+
1390
+ `server.collection<T>(name)` returns a typed handle over the running data. Use it to seed or inspect state in a test without going through HTTP. Combine it with [`yrest types`](#types) to get the item types:
1391
+
1392
+ ```ts
1393
+ import { createYrestServer } from "@yrest/cli";
1394
+ import type { User } from "./db.types"; // npx @yrest/cli types db.yml -o db.types.ts
1395
+
1396
+ const server = createYrestServer({ file: "./tests/db.yml", port: 0 });
1397
+ await server.start();
1398
+
1399
+ const users = server.collection<User>("users");
1400
+ users.create({ name: "Ana", email: "ana@test.com" }); // id assigned by the server
1401
+
1402
+ const res = await fetch(`${server.url}/users`);
1403
+ expect(await res.json()).toHaveLength(users.count());
1404
+ ```
1405
+
1406
+ | Method | Returns | Description |
1407
+ | ------------------- | ---------------- | --------------------------------------------------------------- |
1408
+ | `findAll()` | `T[]` | Every item, in stored order |
1409
+ | `findById(id)` | `T \| undefined` | Item by id — `1` and `"1"` both match |
1410
+ | `findWhere(fn)` | `T[]` | Items for which the predicate returns `true` |
1411
+ | `count()` | `number` | Number of items |
1412
+ | `create(data)` | `T` | Adds an item; the id is generated with the server's id strategy |
1413
+ | `update(id, patch)` | `T \| undefined` | Merges `patch` into the item (the id cannot change) |
1414
+ | `delete(id)` | `T \| undefined` | Removes the item and returns it |
1415
+
1416
+ Changes are visible to HTTP clients immediately and are persisted to `db.yml` exactly like REST writes (in-memory for inline data). Returned items are copies: changing one does not change the stored data — use `update()`. `collection()` throws for an unknown collection, and before `start()`. The same handle is available to handlers as [`ctx.collection`](#handler-functions).
1220
1417
 
1221
1418
  ### `yrest` tagged template literal
1222
1419
 
@@ -1350,7 +1547,8 @@ const server = createYrestServer({
1350
1547
  | Auto-embedding (`nested: true` in `_rel`) | ✅ |
1351
1548
  | SSE stream routes (`_method: SSE`) | ✅ |
1352
1549
  | WebSocket mock channels (`_method: WS`) | 🔜 |
1353
- | TypeScript handlers + typed collections | 🔜 |
1550
+ | TypeScript codegen (`yrest types db.yml`) | ✅ |
1551
+ | TypeScript handlers + typed collections | ✅ |
1354
1552
 
1355
1553
  ---
1356
1554