@yrest/cli 0.13.1 → 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
@@ -11,7 +11,7 @@
11
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)
12
12
  [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
13
13
  [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
14
- [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.13.1)](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)
15
15
 
16
16
  YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
17
17
 
@@ -65,7 +65,7 @@ A YAML-first alternative to json-server for frontend development.
65
65
  | Pageable mode (envelope response) | ✅ | ❌ |
66
66
  | Custom static routes (`_routes`) | ✅ | ❌ |
67
67
  | Template variables in responses | ✅ | ❌ |
68
- | Handler functions (JS logic) | ✅ | ❌ |
68
+ | Handler functions (JS / TS logic) | ✅ | ❌ |
69
69
  | Conditional scenarios (`scenarios:`) | ✅ | ❌ |
70
70
  | Snapshot endpoints | ✅ | ❌ |
71
71
  | Config file | ✅ | ⚠️ |
@@ -73,7 +73,7 @@ A YAML-first alternative to json-server for frontend development.
73
73
  | Watch mode | ✅ | ✅ |
74
74
  | Readonly mode | ✅ | ❌ |
75
75
  | Atomic writes | ✅ | ✅ |
76
- | TypeScript types | ✅ | ❌ |
76
+ | TypeScript types + codegen (`yrest types`) | ✅ | ❌ |
77
77
  | Programmatic API for test frameworks | ✅ | ❌ |
78
78
  | OpenAPI 3.0 spec (`GET /_openapi`, `yrest openapi`) | ✅ | ❌ |
79
79
  | Field annotations (`_schema`) | ✅ | ❌ |
@@ -172,6 +172,104 @@ The spec is also available live at `GET /_openapi` (YAML) and `GET /_openapi.jso
172
172
 
173
173
  ---
174
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
+
175
273
  ### `serve`
176
274
 
177
275
  Starts the mock server.
@@ -196,7 +294,7 @@ npx @yrest/cli serve db.yml --handlers yrest.handlers.js
196
294
  | `--delay <ms>` | `0` | Add a fixed delay to all responses (simulates network latency) |
197
295
  | `--pageable [n]` | `false` | Wrap GET collection responses in `{ data, pagination }`. Optional limit |
198
296
  | `--snapshot` | `false` | Save initial state snapshot and expose `/_snapshot` endpoints |
199
- | `--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 |
200
298
  | `--id-strategy` | `increment` | Id generation for new items: `increment` (auto-int) or `uuid` |
201
299
 
202
300
  All flags can also be set in `yrest.config.yml` (see below). CLI flags always take priority over the config file.
@@ -906,41 +1004,57 @@ _routes:
906
1004
 
907
1005
  ### Handler functions
908
1006
 
909
- 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`:
910
1008
 
911
1009
  ```yaml
912
1010
  _routes:
913
- - method: POST
914
- path: /login
915
- handler: login
916
- response: # optional fallback if handler throws
917
- status: 200
918
- body: { token: fake }
1011
+ - _method: POST
1012
+ _path: /login
1013
+ _handler: login
919
1014
 
920
- - method: GET
921
- path: /auth/me
922
- handler: getCurrentUser
1015
+ - _method: GET
1016
+ _path: /auth/me
1017
+ _handler: getCurrentUser
923
1018
  ```
924
1019
 
925
- 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`):
926
1021
 
927
- ```js
928
- // yrest.handlers.js
929
- export async function login(req) {
930
- const { email, password } = req.body ?? {};
931
- if (password !== "secret") return { status: 401, body: { error: "Invalid credentials" } };
932
- return { status: 200, body: { token: `tok-${email}` } };
933
- }
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
+ });
934
1035
 
935
- export async function getCurrentUser(req) {
936
- return { status: 200, body: { id: 1, name: "Ana", role: "admin" } };
937
- }
1036
+ export const getCurrentUser = defineHandler(() => ({
1037
+ body: { id: 1, name: "Ana", role: "admin" },
1038
+ }));
938
1039
  ```
939
1040
 
940
- Pass the file to the server with `--handlers`:
1041
+ Pass the file to the server with `--handlers` (or `handlers:` in `yrest.config.yml`):
941
1042
 
942
1043
  ```bash
943
- 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
+ }
944
1058
  ```
945
1059
 
946
1060
  **Handler signature:**
@@ -959,10 +1073,24 @@ type HandlerResponse = {
959
1073
  headers?: Record<string, string>;
960
1074
  };
961
1075
 
962
- 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>;
963
1085
  ```
964
1086
 
965
- 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`.
966
1094
 
967
1095
  ### SSE streams
968
1096
 
@@ -1012,6 +1140,41 @@ _events:
1012
1140
  filter: "{{query.filter}}" # from query string
1013
1141
  ```
1014
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
+
1015
1178
  **Consuming from the browser:**
1016
1179
 
1017
1180
  ```js
@@ -1210,16 +1373,47 @@ import { createYrestServer, yrest } from "@yrest/cli";
1210
1373
  | `delay` | `number` | `0` | Fixed delay in ms added to every response |
1211
1374
  | `pageable` | `boolean\|number` | `false` | Wrap GET responses in `{ data, pagination }` envelope |
1212
1375
  | `snapshot` | `boolean` | `false` | Enable snapshot endpoints (`/_snapshot`) |
1213
- | `handlers` | `string` | — | Path to a JS file exporting handler functions |
1376
+ | `handlers` | `string` | — | Path to a JS or TS file exporting handler functions |
1214
1377
 
1215
1378
  **Returned handle:**
1216
1379
 
1217
- | Member | Description |
1218
- | --------- | -------------------------------------------------------- |
1219
- | `start()` | Starts the server and begins listening |
1220
- | `stop()` | Gracefully closes the server |
1221
- | `port` | The actual port after `start()` (useful when `port: 0`) |
1222
- | `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).
1223
1417
 
1224
1418
  ### `yrest` tagged template literal
1225
1419
 
@@ -1353,7 +1547,8 @@ const server = createYrestServer({
1353
1547
  | Auto-embedding (`nested: true` in `_rel`) | ✅ |
1354
1548
  | SSE stream routes (`_method: SSE`) | ✅ |
1355
1549
  | WebSocket mock channels (`_method: WS`) | 🔜 |
1356
- | TypeScript handlers + typed collections | 🔜 |
1550
+ | TypeScript codegen (`yrest types db.yml`) | ✅ |
1551
+ | TypeScript handlers + typed collections | ✅ |
1357
1552
 
1358
1553
  ---
1359
1554