@yrest/cli 0.11.2 → 0.13.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
@@ -10,7 +10,7 @@
10
10
  [![CI](https://github.com/aggiovato/yRest/actions/workflows/ci.yml/badge.svg)](https://github.com/aggiovato/yRest/actions)
11
11
  [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
12
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
13
- [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.11.2)](https://socket.dev/npm/package/@yrest/cli)
13
+ [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.13.0)](https://socket.dev/npm/package/@yrest/cli)
14
14
 
15
15
  YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
16
16
 
@@ -75,6 +75,7 @@ A YAML-first alternative to json-server for frontend development.
75
75
  | OpenAPI 3.0 spec (`GET /_openapi`, `yrest openapi`) | ✅ | ❌ |
76
76
  | Field annotations (`_schema`) | ✅ | ❌ |
77
77
  | Compact relation DSL (`m2o:target@fk[card]+nested`) | ✅ | ❌ |
78
+ | SSE stream routes (`_method: SSE`) | ✅ | ❌ |
78
79
 
79
80
  ---
80
81
 
@@ -256,6 +257,31 @@ _data:
256
257
 
257
258
  Both formats are equivalent. You can also mix them — if the same collection name appears both inside `_data` and at the root level, the root-level entry wins. The file is persisted in whichever format it was originally written.
258
259
 
260
+ ### Data directives (`__uuid_gen`, `__fk`)
261
+
262
+ Use `__uuid_gen` and `__fk` directives to generate UUID-based IDs and cross-collection references automatically. yRest resolves all directives on first load and rewrites the file with real values — subsequent restarts are stable.
263
+
264
+ ```yaml
265
+ users:
266
+ - id: __uuid_gen:ana # generates a UUID and registers alias "ana"
267
+ name: Ana
268
+ - id: __uuid_gen:luis # generates a UUID and registers alias "luis"
269
+ name: Luis
270
+
271
+ posts:
272
+ - id: __uuid_gen # generates a UUID (no alias needed)
273
+ title: Hello World
274
+ userId: __fk.users:ana # resolves to the UUID registered as "ana" in users
275
+ ```
276
+
277
+ | Directive | Meaning |
278
+ | ----------------------- | ---------------------------------------------------------------- |
279
+ | `__uuid_gen` | Generate a UUID v4 for this field |
280
+ | `__uuid_gen:alias` | Generate a UUID and register it under `alias` in this collection |
281
+ | `__fk.collection:alias` | Resolve to the UUID registered as `alias` in `collection` |
282
+
283
+ Aliases are scoped to their collection — `__uuid_gen:ale` in `users` and `__uuid_gen:ale` in `categories` are independent.
284
+
259
285
  ---
260
286
 
261
287
  ## Field annotations (`_schema`)
@@ -935,6 +961,76 @@ type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerRespons
935
961
 
936
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.
937
963
 
964
+ ### SSE streams
965
+
966
+ Mock Server-Sent Events streams by adding `_method: SSE` and an `_sse` block to a `_routes` entry. Clients connect with a regular `GET` request and receive a continuous stream of events until the connection is closed.
967
+
968
+ ```yaml
969
+ _routes:
970
+ - _method: SSE
971
+ _path: /events/orders
972
+ _sse:
973
+ _interval: 1500 # ms between frames (default: 1000)
974
+ _loop: true # restart after last event (default: true)
975
+ _repeat: 3 # stop after N full cycles (omit = infinite)
976
+ _events:
977
+ - _event: update
978
+ _data:
979
+ orderId: 1
980
+ status: processing
981
+ - _event: update
982
+ _data:
983
+ orderId: 1
984
+ status: shipped
985
+ - _event: done
986
+ _data:
987
+ orderId: 1
988
+ status: delivered
989
+ ```
990
+
991
+ **`_sse` options:**
992
+
993
+ | Key | Default | Description |
994
+ | ----------- | ------- | ---------------------------------------------------------------------------------------------------- |
995
+ | `_interval` | `1000` | Milliseconds between frames |
996
+ | `_loop` | `true` | Restart the sequence after the last frame. Set to `false` to close after one cycle |
997
+ | `_repeat` | — | Stop after N complete cycles and close the stream. Omit for infinite |
998
+ | `_events` | — | Ordered list of frames. Each entry needs `_data` (required) and optionally `_event` (SSE event name) |
999
+
1000
+ **Template variables** — resolved per frame at emit time, so each event gets a fresh value:
1001
+
1002
+ ```yaml
1003
+ _events:
1004
+ - _event: tick
1005
+ _data:
1006
+ ts: "{{now}}" # ISO timestamp, unique per frame
1007
+ id: "{{uuid}}" # random UUID v4
1008
+ channel: "{{params.channel}}" # from URL params
1009
+ filter: "{{query.filter}}" # from query string
1010
+ ```
1011
+
1012
+ **Consuming from the browser:**
1013
+
1014
+ ```js
1015
+ const es = new EventSource("http://localhost:3070/events/orders");
1016
+
1017
+ es.addEventListener("update", (e) => console.log(JSON.parse(e.data)));
1018
+ es.addEventListener("done", (e) => {
1019
+ console.log(JSON.parse(e.data));
1020
+ es.close();
1021
+ });
1022
+ ```
1023
+
1024
+ **Consuming from the terminal:**
1025
+
1026
+ ```bash
1027
+ curl -N http://localhost:3070/events/orders
1028
+ ```
1029
+
1030
+ The `-N` flag disables curl's output buffering, which is required to see SSE frames as they arrive.
1031
+
1032
+ A keep-alive comment (`: ping`) is sent every 15 s to prevent proxy timeouts. SSE routes are shown in `/_about` under their own **SSE streams** accordion with a sky-blue `SSE` badge.
1033
+
938
1034
  ---
939
1035
 
940
1036
  ## Server modes
@@ -1252,6 +1348,9 @@ const server = createYrestServer({
1252
1348
  | Explicit relation types (one2one, many2many) | ✅ |
1253
1349
  | Bidirectional many2many nested routes | ✅ |
1254
1350
  | Auto-embedding (`nested: true` in `_rel`) | ✅ |
1351
+ | SSE stream routes (`_method: SSE`) | ✅ |
1352
+ | WebSocket mock channels (`_method: WS`) | 🔜 |
1353
+ | TypeScript handlers + typed collections | 🔜 |
1255
1354
 
1256
1355
  ---
1257
1356
 
package/dist/cli/index.js CHANGED
@@ -115,6 +115,7 @@ categories:
115
115
  // src/cli/commands/templates/relational.ts
116
116
  var relationalTemplate = `# yrest relational sample \u2014 blog
117
117
  # Demonstrates: many2one, one2one and many2many relationships
118
+ # Also shows __uuid_gen and __fk directives for UUID-based IDs
118
119
  # Run: npx @yrest/cli serve db.yml
119
120
  # Docs: GET http://localhost:3070/_about
120
121
 
@@ -139,33 +140,36 @@ _rel:
139
140
  _type: one2one
140
141
  _target: users
141
142
 
143
+ # __uuid_gen:alias generates a UUID and registers it under that alias.
144
+ # __fk.collection:alias resolves to the UUID registered in that collection.
145
+ # yRest rewrites this file on first run, replacing all directives with real values.
142
146
  users:
143
- - id: 1
147
+ - id: __uuid_gen:ana
144
148
  name: Ana Garc\xEDa
145
149
  email: ana@example.com
146
150
  role: author
147
- - id: 2
151
+ - id: __uuid_gen:luis
148
152
  name: Luis Mart\xEDnez
149
153
  email: luis@example.com
150
154
  role: author
151
- - id: 3
155
+ - id: __uuid_gen:sara
152
156
  name: Sara L\xF3pez
153
157
  email: sara@example.com
154
158
  role: reader
155
159
 
156
160
  profiles:
157
- - id: 1
158
- userId: 1
161
+ - id: __uuid_gen
162
+ userId: __fk.users:ana
159
163
  bio: Full-stack developer and open-source enthusiast
160
164
  avatar: https://i.pravatar.cc/150?img=1
161
165
  website: https://ana.dev
162
- - id: 2
163
- userId: 2
166
+ - id: __uuid_gen
167
+ userId: __fk.users:luis
164
168
  bio: Backend engineer, coffee addict
165
169
  avatar: https://i.pravatar.cc/150?img=2
166
170
  website: https://luisdev.io
167
- - id: 3
168
- userId: 3
171
+ - id: __uuid_gen
172
+ userId: __fk.users:sara
169
173
  bio: Designer turned frontend developer
170
174
  avatar: https://i.pravatar.cc/150?img=3
171
175
  website: null
@@ -188,75 +192,75 @@ tags:
188
192
  color: "#ef4444"
189
193
 
190
194
  posts:
191
- - id: 1
195
+ - id: __uuid_gen:post-ts
192
196
  title: Getting started with TypeScript
193
197
  slug: getting-started-typescript
194
198
  body: TypeScript adds static typing to JavaScript, catching errors at compile time...
195
- userId: 1
199
+ userId: __fk.users:ana
196
200
  published: true
197
201
  views: 1420
198
202
  createdAt: "2024-11-01"
199
- - id: 2
203
+ - id: __uuid_gen:post-fastify
200
204
  title: Building REST APIs with Fastify
201
205
  slug: rest-apis-fastify
202
206
  body: Fastify is the fastest Node.js web framework, perfect for building APIs...
203
- userId: 1
207
+ userId: __fk.users:ana
204
208
  published: true
205
209
  views: 980
206
210
  createdAt: "2024-11-15"
207
- - id: 3
211
+ - id: __uuid_gen:post-testing
208
212
  title: Testing strategies for modern apps
209
213
  slug: testing-strategies-modern
210
214
  body: A solid test strategy covers unit, integration and end-to-end scenarios...
211
- userId: 2
215
+ userId: __fk.users:luis
212
216
  published: true
213
217
  views: 640
214
218
  createdAt: "2024-12-03"
215
- - id: 4
219
+ - id: __uuid_gen:post-yaml
216
220
  title: YAML as a database format
217
221
  slug: yaml-database-format
218
222
  body: YAML is human-readable and expressive enough for mock data during development...
219
- userId: 2
223
+ userId: __fk.users:luis
220
224
  published: false
221
225
  views: 0
222
226
  createdAt: "2025-01-10"
223
227
 
224
228
  # pivot table for posts \u2194 tags (many2many)
225
229
  post_tags:
226
- - { id: 1, postId: 1, tagId: 1 }
227
- - { id: 2, postId: 1, tagId: 2 }
228
- - { id: 3, postId: 2, tagId: 2 }
229
- - { id: 4, postId: 2, tagId: 4 }
230
- - { id: 5, postId: 3, tagId: 3 }
231
- - { id: 6, postId: 3, tagId: 1 }
232
- - { id: 7, postId: 4, tagId: 5 }
233
- - { id: 8, postId: 4, tagId: 2 }
230
+ - { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
231
+ - { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
232
+ - { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
233
+ - { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
234
+ - { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
235
+ - { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
236
+ - { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
237
+ - { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
234
238
 
235
239
  comments:
236
240
  - id: 1
237
241
  body: Great introduction! This helped me a lot.
238
- postId: 1
239
- userId: 3
242
+ postId: __fk.posts:post-ts
243
+ userId: __fk.users:sara
240
244
  likes: 5
241
245
  - id: 2
242
246
  body: Could you cover generics in a follow-up post?
243
- postId: 1
244
- userId: 2
247
+ postId: __fk.posts:post-ts
248
+ userId: __fk.users:luis
245
249
  likes: 3
246
250
  - id: 3
247
251
  body: Fastify is indeed much faster than Express in my benchmarks.
248
- postId: 2
249
- userId: 3
252
+ postId: __fk.posts:post-fastify
253
+ userId: __fk.users:sara
250
254
  likes: 8
251
255
  - id: 4
252
256
  body: Do you have a GitHub repo with these examples?
253
- postId: 2
254
- userId: 1
257
+ postId: __fk.posts:post-fastify
258
+ userId: __fk.users:ana
255
259
  likes: 1
256
260
  - id: 5
257
261
  body: E2E tests are underrated. Solid post!
258
- postId: 3
259
- userId: 1
262
+ postId: __fk.posts:post-testing
263
+ userId: __fk.users:ana
260
264
  likes: 4
261
265
 
262
266
  # Try these queries:
@@ -546,7 +550,7 @@ var import_node_path4 = require("path");
546
550
  // src/storage/yrestStorage.ts
547
551
  var import_node_fs2 = require("fs");
548
552
  var import_node_path2 = require("path");
549
- var import_node_crypto = require("crypto");
553
+ var import_node_crypto2 = require("crypto");
550
554
  var import_yaml = require("yaml");
551
555
 
552
556
  // src/storage/parseRelations.ts
@@ -664,6 +668,8 @@ function normaliseRoute(raw) {
664
668
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
665
669
  const v = raw;
666
670
  const method = v["_method"];
671
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
672
+ if ("_sse" in v) return null;
667
673
  const path = v["_path"];
668
674
  if (typeof method !== "string" || typeof path !== "string") return null;
669
675
  const route = { method, path };
@@ -703,6 +709,43 @@ function normaliseScenario(raw) {
703
709
  return { when, response };
704
710
  }
705
711
 
712
+ // src/storage/parseSseRoutes.ts
713
+ function parseSseRoutes(raw) {
714
+ if (!Array.isArray(raw)) return [];
715
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
716
+ }
717
+ function normaliseSseRoute(raw) {
718
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
719
+ const v = raw;
720
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
721
+ const path = v["_path"];
722
+ const sse = v["_sse"];
723
+ if (method !== "SSE") return null;
724
+ if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
725
+ return null;
726
+ }
727
+ const s = sse;
728
+ const rawEvents = s["_events"];
729
+ if (!Array.isArray(rawEvents)) return null;
730
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
731
+ if (!events.length) return null;
732
+ return {
733
+ path,
734
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
735
+ loop: s["_loop"] !== false,
736
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
737
+ events
738
+ };
739
+ }
740
+ function normaliseEvent(raw) {
741
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
742
+ const v = raw;
743
+ if (!("_data" in v)) return null;
744
+ const event = { data: v["_data"] };
745
+ if (typeof v["_event"] === "string") event.event = v["_event"];
746
+ return event;
747
+ }
748
+
706
749
  // src/storage/parseSchema.ts
707
750
  function parseSchema(raw) {
708
751
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -779,6 +822,51 @@ function hasDataBlock(raw) {
779
822
  return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
780
823
  }
781
824
 
825
+ // src/storage/resolveDirectives.ts
826
+ var import_node_crypto = require("crypto");
827
+ var UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
828
+ var FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
829
+ function resolveDirectives(data) {
830
+ const registry = /* @__PURE__ */ new Map();
831
+ let modified = false;
832
+ for (const [collection, items] of Object.entries(data)) {
833
+ for (const item of items) {
834
+ for (const [field, value] of Object.entries(item)) {
835
+ if (typeof value !== "string") continue;
836
+ const m = UUID_GEN_RE.exec(value);
837
+ if (!m) continue;
838
+ const uuid = (0, import_node_crypto.randomUUID)();
839
+ item[field] = uuid;
840
+ modified = true;
841
+ const alias = m[1];
842
+ if (alias) {
843
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
844
+ registry.get(collection).set(alias, uuid);
845
+ }
846
+ }
847
+ }
848
+ }
849
+ for (const items of Object.values(data)) {
850
+ for (const item of items) {
851
+ for (const [field, value] of Object.entries(item)) {
852
+ if (typeof value !== "string") continue;
853
+ const m = FK_RE.exec(value);
854
+ if (!m) continue;
855
+ const col = m[1];
856
+ const alias = m[2];
857
+ const uuid = registry.get(col)?.get(alias);
858
+ if (uuid !== void 0) {
859
+ item[field] = uuid;
860
+ modified = true;
861
+ } else {
862
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
863
+ }
864
+ }
865
+ }
866
+ }
867
+ return modified;
868
+ }
869
+
782
870
  // src/utils/deepCopy.ts
783
871
  function deepCopyData(source) {
784
872
  return Object.fromEntries(
@@ -846,15 +934,17 @@ function createYrestStorage(filePath) {
846
934
  const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
847
935
  const relations = parseRelations(raw["_rel"]);
848
936
  const routes = parseRoutes(raw["_routes"]);
937
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
849
938
  const schema = parseSchema(raw["_schema"]);
850
939
  const dataBlock = hasDataBlock(raw);
851
940
  const data = parseData(raw);
941
+ const directivesModified = resolveDirectives(data);
852
942
  let snapshot = {
853
943
  data: deepCopyData(data),
854
944
  relations: { ...relations },
855
945
  savedAt: /* @__PURE__ */ new Date()
856
946
  };
857
- return {
947
+ const storage = {
858
948
  getData() {
859
949
  return data;
860
950
  },
@@ -867,6 +957,9 @@ function createYrestStorage(filePath) {
867
957
  getRoutes() {
868
958
  return routes;
869
959
  },
960
+ getSseRoutes() {
961
+ return sseRoutes;
962
+ },
870
963
  getCollection(name) {
871
964
  return data[name];
872
965
  },
@@ -882,7 +975,7 @@ function createYrestStorage(filePath) {
882
975
  } else {
883
976
  Object.assign(payload, data);
884
977
  }
885
- const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto.randomUUID)()}.tmp`);
978
+ const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
886
979
  (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml.stringify)(payload), "utf8");
887
980
  (0, import_node_fs2.renameSync)(tmp, absPath);
888
981
  },
@@ -894,6 +987,7 @@ function createYrestStorage(filePath) {
894
987
  Object.assign(data, freshData);
895
988
  for (const key of Object.keys(relations)) delete relations[key];
896
989
  Object.assign(relations, freshRelations);
990
+ if (resolveDirectives(data)) storage.persist();
897
991
  },
898
992
  getSnapshot() {
899
993
  return snapshot;
@@ -914,6 +1008,8 @@ function createYrestStorage(filePath) {
914
1008
  this.persist();
915
1009
  }
916
1010
  };
1011
+ if (directivesModified) storage.persist();
1012
+ return storage;
917
1013
  }
918
1014
 
919
1015
  // src/server/createServer.ts
@@ -926,7 +1022,7 @@ var import_node_path3 = require("path");
926
1022
  var import_node_url = require("url");
927
1023
 
928
1024
  // src/utils/interpolate.ts
929
- var import_node_crypto2 = require("crypto");
1025
+ var import_node_crypto3 = require("crypto");
930
1026
  function getPath(obj, path) {
931
1027
  return path.split(".").reduce((acc, key) => {
932
1028
  if (acc != null && typeof acc === "object") return acc[key];
@@ -935,7 +1031,7 @@ function getPath(obj, path) {
935
1031
  }
936
1032
  function resolveVar(path, ctx) {
937
1033
  if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
938
- if (path === "uuid") return (0, import_node_crypto2.randomUUID)();
1034
+ if (path === "uuid") return (0, import_node_crypto3.randomUUID)();
939
1035
  if (path === "body") return ctx.body;
940
1036
  if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
941
1037
  if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
@@ -978,6 +1074,8 @@ var METHOD_COLOR = {
978
1074
  PUT: "#d29922",
979
1075
  PATCH: "#a371f7",
980
1076
  DELETE: "#f85149",
1077
+ SSE: "#0ea5e9",
1078
+ WS: "#8b5cf6",
981
1079
  fn: "#f0883e"
982
1080
  };
983
1081
  function escapeHtml(str) {
@@ -1169,6 +1267,30 @@ function handlersAccordion(handlers, routes, base) {
1169
1267
  </tbody></table>
1170
1268
  </details>`;
1171
1269
  }
1270
+ function sseRoutesAccordion(routes, base) {
1271
+ if (!routes.length) return "";
1272
+ const rows = routes.map((r) => {
1273
+ const tags = [
1274
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
1275
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
1276
+ ];
1277
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
1278
+ if (r.repeat !== void 0)
1279
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
1280
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
1281
+ return endpointRow("SSE", `${base}${r.path}`, desc);
1282
+ });
1283
+ return `
1284
+ <details class="resource-card nested-card">
1285
+ <summary>
1286
+ <span class="resource-name">SSE streams</span>
1287
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
1288
+ </summary>
1289
+ <table><tbody>
1290
+ ${rows.join("")}
1291
+ </tbody></table>
1292
+ </details>`;
1293
+ }
1172
1294
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
1173
1295
  const examples = [];
1174
1296
  const firstCol = collections[0];
@@ -1839,6 +1961,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1839
1961
  const collections = Object.keys(storage.getData());
1840
1962
  const relations = storage.getRelations();
1841
1963
  const customRoutes = storage.getRoutes();
1964
+ const sseRoutes = storage.getSseRoutes();
1842
1965
  const base = options.base;
1843
1966
  const host = `http://${options.host}:${options.port}`;
1844
1967
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -2022,6 +2145,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
2022
2145
  ${accordions}
2023
2146
  ${nestedRoutesAccordion(relations, base)}
2024
2147
  ${options.snapshot ? snapshotAccordion() : ""}
2148
+ ${sseRoutesAccordion(sseRoutes, base)}
2025
2149
  ${customRoutesAccordion(customRoutes, base, handlers)}
2026
2150
  ${handlersAccordion(handlers, customRoutes, base)}
2027
2151
  </div>
@@ -3152,6 +3276,79 @@ var SnapshotRouteCommand = class {
3152
3276
  }
3153
3277
  };
3154
3278
 
3279
+ // src/router/routes/sse.routes.ts
3280
+ var SSERouteCommand = class {
3281
+ constructor(storage, base) {
3282
+ this.storage = storage;
3283
+ this.base = base;
3284
+ }
3285
+ storage;
3286
+ base;
3287
+ register(server) {
3288
+ for (const route of this.storage.getSseRoutes()) {
3289
+ const url = `${this.base}${route.path}`;
3290
+ server.route({
3291
+ method: "GET",
3292
+ url,
3293
+ handler: async (req, reply) => {
3294
+ reply.hijack();
3295
+ const raw = reply.raw;
3296
+ raw.setHeader("Content-Type", "text/event-stream");
3297
+ raw.setHeader("Cache-Control", "no-cache");
3298
+ raw.setHeader("Connection", "keep-alive");
3299
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3300
+ raw.flushHeaders();
3301
+ let alive = true;
3302
+ req.raw.on("close", () => {
3303
+ alive = false;
3304
+ });
3305
+ const keepAlive = setInterval(() => {
3306
+ if (alive) raw.write(": ping\n\n");
3307
+ }, 15e3);
3308
+ let frameIndex = 0;
3309
+ let cycleCount = 0;
3310
+ function sendNext() {
3311
+ if (!alive) {
3312
+ clearInterval(keepAlive);
3313
+ return;
3314
+ }
3315
+ const ev = route.events[frameIndex];
3316
+ const ctx = {
3317
+ params: req.params,
3318
+ query: req.query,
3319
+ body: null,
3320
+ headers: req.headers
3321
+ };
3322
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
3323
+ let chunk = "";
3324
+ if (ev.event) chunk += `event: ${ev.event}
3325
+ `;
3326
+ chunk += `data: ${JSON.stringify(data)}
3327
+
3328
+ `;
3329
+ raw.write(chunk);
3330
+ frameIndex++;
3331
+ if (frameIndex >= route.events.length) {
3332
+ frameIndex = 0;
3333
+ cycleCount++;
3334
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
3335
+ if (done) {
3336
+ clearInterval(keepAlive);
3337
+ raw.end();
3338
+ return;
3339
+ }
3340
+ }
3341
+ if (alive) {
3342
+ setTimeout(sendNext, route.interval);
3343
+ }
3344
+ }
3345
+ sendNext();
3346
+ }
3347
+ });
3348
+ }
3349
+ }
3350
+ };
3351
+
3155
3352
  // src/router/resource.router.ts
3156
3353
  function buildResourceRouteCommands(storage, options) {
3157
3354
  const commands = [];
@@ -3193,6 +3390,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3193
3390
  new AboutRouteCommand(storage, options, handlers),
3194
3391
  new OpenApiRouteCommand(storage, options),
3195
3392
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3393
+ new SSERouteCommand(storage, options.base),
3196
3394
  new CustomRouteCommand(storage, options.base, handlers),
3197
3395
  ...buildResourceRouteCommands(storage, options)
3198
3396
  ];
@@ -3338,11 +3536,18 @@ function registerServe(program2) {
3338
3536
  "--id-strategy <strategy>",
3339
3537
  "Id generation strategy for new items: increment (default) or uuid",
3340
3538
  "increment"
3539
+ ).option(
3540
+ "--uuid",
3541
+ "Shorthand for --id-strategy uuid: assign UUID ids to new items created via POST"
3341
3542
  ).action(async (file, flags, cmd) => {
3342
3543
  const fileConfig = loadConfigFile((0, import_node_path4.join)(process.cwd(), "yrest.config.yml"));
3343
3544
  const cliOverrides = Object.fromEntries(
3344
3545
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3345
3546
  );
3547
+ if (cliOverrides["uuid"] === true) {
3548
+ cliOverrides["idStrategy"] = "uuid";
3549
+ delete cliOverrides["uuid"];
3550
+ }
3346
3551
  const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3347
3552
  let storage;
3348
3553
  try {