@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.
@@ -88,6 +88,7 @@ categories:
88
88
  // src/cli/commands/templates/relational.ts
89
89
  var relationalTemplate = `# yrest relational sample \u2014 blog
90
90
  # Demonstrates: many2one, one2one and many2many relationships
91
+ # Also shows __uuid_gen and __fk directives for UUID-based IDs
91
92
  # Run: npx @yrest/cli serve db.yml
92
93
  # Docs: GET http://localhost:3070/_about
93
94
 
@@ -112,33 +113,36 @@ _rel:
112
113
  _type: one2one
113
114
  _target: users
114
115
 
116
+ # __uuid_gen:alias generates a UUID and registers it under that alias.
117
+ # __fk.collection:alias resolves to the UUID registered in that collection.
118
+ # yRest rewrites this file on first run, replacing all directives with real values.
115
119
  users:
116
- - id: 1
120
+ - id: __uuid_gen:ana
117
121
  name: Ana Garc\xEDa
118
122
  email: ana@example.com
119
123
  role: author
120
- - id: 2
124
+ - id: __uuid_gen:luis
121
125
  name: Luis Mart\xEDnez
122
126
  email: luis@example.com
123
127
  role: author
124
- - id: 3
128
+ - id: __uuid_gen:sara
125
129
  name: Sara L\xF3pez
126
130
  email: sara@example.com
127
131
  role: reader
128
132
 
129
133
  profiles:
130
- - id: 1
131
- userId: 1
134
+ - id: __uuid_gen
135
+ userId: __fk.users:ana
132
136
  bio: Full-stack developer and open-source enthusiast
133
137
  avatar: https://i.pravatar.cc/150?img=1
134
138
  website: https://ana.dev
135
- - id: 2
136
- userId: 2
139
+ - id: __uuid_gen
140
+ userId: __fk.users:luis
137
141
  bio: Backend engineer, coffee addict
138
142
  avatar: https://i.pravatar.cc/150?img=2
139
143
  website: https://luisdev.io
140
- - id: 3
141
- userId: 3
144
+ - id: __uuid_gen
145
+ userId: __fk.users:sara
142
146
  bio: Designer turned frontend developer
143
147
  avatar: https://i.pravatar.cc/150?img=3
144
148
  website: null
@@ -161,75 +165,75 @@ tags:
161
165
  color: "#ef4444"
162
166
 
163
167
  posts:
164
- - id: 1
168
+ - id: __uuid_gen:post-ts
165
169
  title: Getting started with TypeScript
166
170
  slug: getting-started-typescript
167
171
  body: TypeScript adds static typing to JavaScript, catching errors at compile time...
168
- userId: 1
172
+ userId: __fk.users:ana
169
173
  published: true
170
174
  views: 1420
171
175
  createdAt: "2024-11-01"
172
- - id: 2
176
+ - id: __uuid_gen:post-fastify
173
177
  title: Building REST APIs with Fastify
174
178
  slug: rest-apis-fastify
175
179
  body: Fastify is the fastest Node.js web framework, perfect for building APIs...
176
- userId: 1
180
+ userId: __fk.users:ana
177
181
  published: true
178
182
  views: 980
179
183
  createdAt: "2024-11-15"
180
- - id: 3
184
+ - id: __uuid_gen:post-testing
181
185
  title: Testing strategies for modern apps
182
186
  slug: testing-strategies-modern
183
187
  body: A solid test strategy covers unit, integration and end-to-end scenarios...
184
- userId: 2
188
+ userId: __fk.users:luis
185
189
  published: true
186
190
  views: 640
187
191
  createdAt: "2024-12-03"
188
- - id: 4
192
+ - id: __uuid_gen:post-yaml
189
193
  title: YAML as a database format
190
194
  slug: yaml-database-format
191
195
  body: YAML is human-readable and expressive enough for mock data during development...
192
- userId: 2
196
+ userId: __fk.users:luis
193
197
  published: false
194
198
  views: 0
195
199
  createdAt: "2025-01-10"
196
200
 
197
201
  # pivot table for posts \u2194 tags (many2many)
198
202
  post_tags:
199
- - { id: 1, postId: 1, tagId: 1 }
200
- - { id: 2, postId: 1, tagId: 2 }
201
- - { id: 3, postId: 2, tagId: 2 }
202
- - { id: 4, postId: 2, tagId: 4 }
203
- - { id: 5, postId: 3, tagId: 3 }
204
- - { id: 6, postId: 3, tagId: 1 }
205
- - { id: 7, postId: 4, tagId: 5 }
206
- - { id: 8, postId: 4, tagId: 2 }
203
+ - { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
204
+ - { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
205
+ - { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
206
+ - { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
207
+ - { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
208
+ - { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
209
+ - { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
210
+ - { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
207
211
 
208
212
  comments:
209
213
  - id: 1
210
214
  body: Great introduction! This helped me a lot.
211
- postId: 1
212
- userId: 3
215
+ postId: __fk.posts:post-ts
216
+ userId: __fk.users:sara
213
217
  likes: 5
214
218
  - id: 2
215
219
  body: Could you cover generics in a follow-up post?
216
- postId: 1
217
- userId: 2
220
+ postId: __fk.posts:post-ts
221
+ userId: __fk.users:luis
218
222
  likes: 3
219
223
  - id: 3
220
224
  body: Fastify is indeed much faster than Express in my benchmarks.
221
- postId: 2
222
- userId: 3
225
+ postId: __fk.posts:post-fastify
226
+ userId: __fk.users:sara
223
227
  likes: 8
224
228
  - id: 4
225
229
  body: Do you have a GitHub repo with these examples?
226
- postId: 2
227
- userId: 1
230
+ postId: __fk.posts:post-fastify
231
+ userId: __fk.users:ana
228
232
  likes: 1
229
233
  - id: 5
230
234
  body: E2E tests are underrated. Solid post!
231
- postId: 3
232
- userId: 1
235
+ postId: __fk.posts:post-testing
236
+ userId: __fk.users:ana
233
237
  likes: 4
234
238
 
235
239
  # Try these queries:
@@ -519,7 +523,7 @@ import { join as join2, resolve as resolve3 } from "path";
519
523
  // src/storage/yrestStorage.ts
520
524
  import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
521
525
  import { resolve as resolve2, dirname } from "path";
522
- import { randomUUID } from "crypto";
526
+ import { randomUUID as randomUUID2 } from "crypto";
523
527
  import { parse, stringify } from "yaml";
524
528
 
525
529
  // src/storage/parseRelations.ts
@@ -637,6 +641,8 @@ function normaliseRoute(raw) {
637
641
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
638
642
  const v = raw;
639
643
  const method = v["_method"];
644
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
645
+ if ("_sse" in v) return null;
640
646
  const path = v["_path"];
641
647
  if (typeof method !== "string" || typeof path !== "string") return null;
642
648
  const route = { method, path };
@@ -676,6 +682,43 @@ function normaliseScenario(raw) {
676
682
  return { when, response };
677
683
  }
678
684
 
685
+ // src/storage/parseSseRoutes.ts
686
+ function parseSseRoutes(raw) {
687
+ if (!Array.isArray(raw)) return [];
688
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
689
+ }
690
+ function normaliseSseRoute(raw) {
691
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
692
+ const v = raw;
693
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
694
+ const path = v["_path"];
695
+ const sse = v["_sse"];
696
+ if (method !== "SSE") return null;
697
+ if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
698
+ return null;
699
+ }
700
+ const s = sse;
701
+ const rawEvents = s["_events"];
702
+ if (!Array.isArray(rawEvents)) return null;
703
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
704
+ if (!events.length) return null;
705
+ return {
706
+ path,
707
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
708
+ loop: s["_loop"] !== false,
709
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
710
+ events
711
+ };
712
+ }
713
+ function normaliseEvent(raw) {
714
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
715
+ const v = raw;
716
+ if (!("_data" in v)) return null;
717
+ const event = { data: v["_data"] };
718
+ if (typeof v["_event"] === "string") event.event = v["_event"];
719
+ return event;
720
+ }
721
+
679
722
  // src/storage/parseSchema.ts
680
723
  function parseSchema(raw) {
681
724
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -752,6 +795,51 @@ function hasDataBlock(raw) {
752
795
  return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
753
796
  }
754
797
 
798
+ // src/storage/resolveDirectives.ts
799
+ import { randomUUID } from "crypto";
800
+ var UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
801
+ var FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
802
+ function resolveDirectives(data) {
803
+ const registry = /* @__PURE__ */ new Map();
804
+ let modified = false;
805
+ for (const [collection, items] of Object.entries(data)) {
806
+ for (const item of items) {
807
+ for (const [field, value] of Object.entries(item)) {
808
+ if (typeof value !== "string") continue;
809
+ const m = UUID_GEN_RE.exec(value);
810
+ if (!m) continue;
811
+ const uuid = randomUUID();
812
+ item[field] = uuid;
813
+ modified = true;
814
+ const alias = m[1];
815
+ if (alias) {
816
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
817
+ registry.get(collection).set(alias, uuid);
818
+ }
819
+ }
820
+ }
821
+ }
822
+ for (const items of Object.values(data)) {
823
+ for (const item of items) {
824
+ for (const [field, value] of Object.entries(item)) {
825
+ if (typeof value !== "string") continue;
826
+ const m = FK_RE.exec(value);
827
+ if (!m) continue;
828
+ const col = m[1];
829
+ const alias = m[2];
830
+ const uuid = registry.get(col)?.get(alias);
831
+ if (uuid !== void 0) {
832
+ item[field] = uuid;
833
+ modified = true;
834
+ } else {
835
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
836
+ }
837
+ }
838
+ }
839
+ }
840
+ return modified;
841
+ }
842
+
755
843
  // src/utils/deepCopy.ts
756
844
  function deepCopyData(source) {
757
845
  return Object.fromEntries(
@@ -819,15 +907,17 @@ function createYrestStorage(filePath) {
819
907
  const raw = parse(readFileSync(absPath, "utf8")) ?? {};
820
908
  const relations = parseRelations(raw["_rel"]);
821
909
  const routes = parseRoutes(raw["_routes"]);
910
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
822
911
  const schema = parseSchema(raw["_schema"]);
823
912
  const dataBlock = hasDataBlock(raw);
824
913
  const data = parseData(raw);
914
+ const directivesModified = resolveDirectives(data);
825
915
  let snapshot = {
826
916
  data: deepCopyData(data),
827
917
  relations: { ...relations },
828
918
  savedAt: /* @__PURE__ */ new Date()
829
919
  };
830
- return {
920
+ const storage = {
831
921
  getData() {
832
922
  return data;
833
923
  },
@@ -840,6 +930,9 @@ function createYrestStorage(filePath) {
840
930
  getRoutes() {
841
931
  return routes;
842
932
  },
933
+ getSseRoutes() {
934
+ return sseRoutes;
935
+ },
843
936
  getCollection(name) {
844
937
  return data[name];
845
938
  },
@@ -855,7 +948,7 @@ function createYrestStorage(filePath) {
855
948
  } else {
856
949
  Object.assign(payload, data);
857
950
  }
858
- const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID()}.tmp`);
951
+ const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID2()}.tmp`);
859
952
  writeFileSync2(tmp, stringify(payload), "utf8");
860
953
  renameSync(tmp, absPath);
861
954
  },
@@ -867,6 +960,7 @@ function createYrestStorage(filePath) {
867
960
  Object.assign(data, freshData);
868
961
  for (const key of Object.keys(relations)) delete relations[key];
869
962
  Object.assign(relations, freshRelations);
963
+ if (resolveDirectives(data)) storage.persist();
870
964
  },
871
965
  getSnapshot() {
872
966
  return snapshot;
@@ -887,6 +981,8 @@ function createYrestStorage(filePath) {
887
981
  this.persist();
888
982
  }
889
983
  };
984
+ if (directivesModified) storage.persist();
985
+ return storage;
890
986
  }
891
987
 
892
988
  // src/server/createServer.ts
@@ -899,7 +995,7 @@ import { dirname as dirname2, join } from "path";
899
995
  import { fileURLToPath } from "url";
900
996
 
901
997
  // src/utils/interpolate.ts
902
- import { randomUUID as randomUUID2 } from "crypto";
998
+ import { randomUUID as randomUUID3 } from "crypto";
903
999
  function getPath(obj, path) {
904
1000
  return path.split(".").reduce((acc, key) => {
905
1001
  if (acc != null && typeof acc === "object") return acc[key];
@@ -908,7 +1004,7 @@ function getPath(obj, path) {
908
1004
  }
909
1005
  function resolveVar(path, ctx) {
910
1006
  if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
911
- if (path === "uuid") return randomUUID2();
1007
+ if (path === "uuid") return randomUUID3();
912
1008
  if (path === "body") return ctx.body;
913
1009
  if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
914
1010
  if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
@@ -951,6 +1047,8 @@ var METHOD_COLOR = {
951
1047
  PUT: "#d29922",
952
1048
  PATCH: "#a371f7",
953
1049
  DELETE: "#f85149",
1050
+ SSE: "#0ea5e9",
1051
+ WS: "#8b5cf6",
954
1052
  fn: "#f0883e"
955
1053
  };
956
1054
  function escapeHtml(str) {
@@ -1142,6 +1240,30 @@ function handlersAccordion(handlers, routes, base) {
1142
1240
  </tbody></table>
1143
1241
  </details>`;
1144
1242
  }
1243
+ function sseRoutesAccordion(routes, base) {
1244
+ if (!routes.length) return "";
1245
+ const rows = routes.map((r) => {
1246
+ const tags = [
1247
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
1248
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
1249
+ ];
1250
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
1251
+ if (r.repeat !== void 0)
1252
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
1253
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
1254
+ return endpointRow("SSE", `${base}${r.path}`, desc);
1255
+ });
1256
+ return `
1257
+ <details class="resource-card nested-card">
1258
+ <summary>
1259
+ <span class="resource-name">SSE streams</span>
1260
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
1261
+ </summary>
1262
+ <table><tbody>
1263
+ ${rows.join("")}
1264
+ </tbody></table>
1265
+ </details>`;
1266
+ }
1145
1267
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
1146
1268
  const examples = [];
1147
1269
  const firstCol = collections[0];
@@ -1812,6 +1934,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1812
1934
  const collections = Object.keys(storage.getData());
1813
1935
  const relations = storage.getRelations();
1814
1936
  const customRoutes = storage.getRoutes();
1937
+ const sseRoutes = storage.getSseRoutes();
1815
1938
  const base = options.base;
1816
1939
  const host = `http://${options.host}:${options.port}`;
1817
1940
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1995,6 +2118,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1995
2118
  ${accordions}
1996
2119
  ${nestedRoutesAccordion(relations, base)}
1997
2120
  ${options.snapshot ? snapshotAccordion() : ""}
2121
+ ${sseRoutesAccordion(sseRoutes, base)}
1998
2122
  ${customRoutesAccordion(customRoutes, base, handlers)}
1999
2123
  ${handlersAccordion(handlers, customRoutes, base)}
2000
2124
  </div>
@@ -3125,6 +3249,79 @@ var SnapshotRouteCommand = class {
3125
3249
  }
3126
3250
  };
3127
3251
 
3252
+ // src/router/routes/sse.routes.ts
3253
+ var SSERouteCommand = class {
3254
+ constructor(storage, base) {
3255
+ this.storage = storage;
3256
+ this.base = base;
3257
+ }
3258
+ storage;
3259
+ base;
3260
+ register(server) {
3261
+ for (const route of this.storage.getSseRoutes()) {
3262
+ const url = `${this.base}${route.path}`;
3263
+ server.route({
3264
+ method: "GET",
3265
+ url,
3266
+ handler: async (req, reply) => {
3267
+ reply.hijack();
3268
+ const raw = reply.raw;
3269
+ raw.setHeader("Content-Type", "text/event-stream");
3270
+ raw.setHeader("Cache-Control", "no-cache");
3271
+ raw.setHeader("Connection", "keep-alive");
3272
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3273
+ raw.flushHeaders();
3274
+ let alive = true;
3275
+ req.raw.on("close", () => {
3276
+ alive = false;
3277
+ });
3278
+ const keepAlive = setInterval(() => {
3279
+ if (alive) raw.write(": ping\n\n");
3280
+ }, 15e3);
3281
+ let frameIndex = 0;
3282
+ let cycleCount = 0;
3283
+ function sendNext() {
3284
+ if (!alive) {
3285
+ clearInterval(keepAlive);
3286
+ return;
3287
+ }
3288
+ const ev = route.events[frameIndex];
3289
+ const ctx = {
3290
+ params: req.params,
3291
+ query: req.query,
3292
+ body: null,
3293
+ headers: req.headers
3294
+ };
3295
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
3296
+ let chunk = "";
3297
+ if (ev.event) chunk += `event: ${ev.event}
3298
+ `;
3299
+ chunk += `data: ${JSON.stringify(data)}
3300
+
3301
+ `;
3302
+ raw.write(chunk);
3303
+ frameIndex++;
3304
+ if (frameIndex >= route.events.length) {
3305
+ frameIndex = 0;
3306
+ cycleCount++;
3307
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
3308
+ if (done) {
3309
+ clearInterval(keepAlive);
3310
+ raw.end();
3311
+ return;
3312
+ }
3313
+ }
3314
+ if (alive) {
3315
+ setTimeout(sendNext, route.interval);
3316
+ }
3317
+ }
3318
+ sendNext();
3319
+ }
3320
+ });
3321
+ }
3322
+ }
3323
+ };
3324
+
3128
3325
  // src/router/resource.router.ts
3129
3326
  function buildResourceRouteCommands(storage, options) {
3130
3327
  const commands = [];
@@ -3166,6 +3363,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3166
3363
  new AboutRouteCommand(storage, options, handlers),
3167
3364
  new OpenApiRouteCommand(storage, options),
3168
3365
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3366
+ new SSERouteCommand(storage, options.base),
3169
3367
  new CustomRouteCommand(storage, options.base, handlers),
3170
3368
  ...buildResourceRouteCommands(storage, options)
3171
3369
  ];
@@ -3311,11 +3509,18 @@ function registerServe(program2) {
3311
3509
  "--id-strategy <strategy>",
3312
3510
  "Id generation strategy for new items: increment (default) or uuid",
3313
3511
  "increment"
3512
+ ).option(
3513
+ "--uuid",
3514
+ "Shorthand for --id-strategy uuid: assign UUID ids to new items created via POST"
3314
3515
  ).action(async (file, flags, cmd) => {
3315
3516
  const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
3316
3517
  const cliOverrides = Object.fromEntries(
3317
3518
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3318
3519
  );
3520
+ if (cliOverrides["uuid"] === true) {
3521
+ cliOverrides["idStrategy"] = "uuid";
3522
+ delete cliOverrides["uuid"];
3523
+ }
3319
3524
  const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3320
3525
  let storage;
3321
3526
  try {
package/dist/index.d.mts CHANGED
@@ -284,6 +284,42 @@ type CustomRoute = {
284
284
  /** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
285
285
  errorBody?: unknown;
286
286
  };
287
+ /** A single event in an SSE stream declared under `_sse._events`. */
288
+ type SseEvent = {
289
+ /** SSE event name emitted as `event: <name>`. Omit for unnamed events. */
290
+ event?: string;
291
+ /** Payload serialised as JSON in the `data:` line. Supports `{{}}` templates. */
292
+ data: unknown;
293
+ };
294
+ /**
295
+ * A Server-Sent Events stream route declared under `_routes` with an `_sse` block.
296
+ *
297
+ * @example
298
+ * ```yaml
299
+ * _routes:
300
+ * - _method: GET
301
+ * _path: /events/orders
302
+ * _sse:
303
+ * _interval: 1500
304
+ * _loop: true
305
+ * _repeat: 5
306
+ * _events:
307
+ * - _event: update
308
+ * _data: { orderId: 1, status: processing }
309
+ * ```
310
+ */
311
+ type SseRoute = {
312
+ /** URL path. May include Fastify path params (e.g. `/events/:id`). */
313
+ path: string;
314
+ /** Milliseconds between frames. Default: `1000`. */
315
+ interval: number;
316
+ /** Restart the sequence after the last event. Default: `true`. */
317
+ loop: boolean;
318
+ /** Stop after this many full cycles. Omit for infinite. */
319
+ repeat?: number;
320
+ /** Ordered list of events to emit. */
321
+ events: SseEvent[];
322
+ };
287
323
  /**
288
324
  * In-memory store backed by a YAML file.
289
325
  *
@@ -336,6 +372,11 @@ interface YrestStorage {
336
372
  * Returns an empty array if no `_routes` block is present.
337
373
  */
338
374
  getRoutes(): CustomRoute[];
375
+ /**
376
+ * Returns the SSE stream routes declared under `_routes` (entries with an `_sse` block).
377
+ * Returns an empty array if none are present.
378
+ */
379
+ getSseRoutes(): SseRoute[];
339
380
  /**
340
381
  * Returns the saved snapshot: a frozen copy of `data` and `relations` taken
341
382
  * at the last call to {@link saveSnapshot} (or at construction time).
package/dist/index.d.ts CHANGED
@@ -284,6 +284,42 @@ type CustomRoute = {
284
284
  /** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
285
285
  errorBody?: unknown;
286
286
  };
287
+ /** A single event in an SSE stream declared under `_sse._events`. */
288
+ type SseEvent = {
289
+ /** SSE event name emitted as `event: <name>`. Omit for unnamed events. */
290
+ event?: string;
291
+ /** Payload serialised as JSON in the `data:` line. Supports `{{}}` templates. */
292
+ data: unknown;
293
+ };
294
+ /**
295
+ * A Server-Sent Events stream route declared under `_routes` with an `_sse` block.
296
+ *
297
+ * @example
298
+ * ```yaml
299
+ * _routes:
300
+ * - _method: GET
301
+ * _path: /events/orders
302
+ * _sse:
303
+ * _interval: 1500
304
+ * _loop: true
305
+ * _repeat: 5
306
+ * _events:
307
+ * - _event: update
308
+ * _data: { orderId: 1, status: processing }
309
+ * ```
310
+ */
311
+ type SseRoute = {
312
+ /** URL path. May include Fastify path params (e.g. `/events/:id`). */
313
+ path: string;
314
+ /** Milliseconds between frames. Default: `1000`. */
315
+ interval: number;
316
+ /** Restart the sequence after the last event. Default: `true`. */
317
+ loop: boolean;
318
+ /** Stop after this many full cycles. Omit for infinite. */
319
+ repeat?: number;
320
+ /** Ordered list of events to emit. */
321
+ events: SseEvent[];
322
+ };
287
323
  /**
288
324
  * In-memory store backed by a YAML file.
289
325
  *
@@ -336,6 +372,11 @@ interface YrestStorage {
336
372
  * Returns an empty array if no `_routes` block is present.
337
373
  */
338
374
  getRoutes(): CustomRoute[];
375
+ /**
376
+ * Returns the SSE stream routes declared under `_routes` (entries with an `_sse` block).
377
+ * Returns an empty array if none are present.
378
+ */
379
+ getSseRoutes(): SseRoute[];
339
380
  /**
340
381
  * Returns the saved snapshot: a frozen copy of `data` and `relations` taken
341
382
  * at the last call to {@link saveSnapshot} (or at construction time).