@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/dist/index.mjs CHANGED
@@ -130,6 +130,49 @@ var init_parseRelations = __esm({
130
130
  }
131
131
  });
132
132
 
133
+ // src/storage/parseSseRoutes.ts
134
+ function parseSseRoutes(raw) {
135
+ if (!Array.isArray(raw)) return [];
136
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
137
+ }
138
+ function normaliseSseRoute(raw) {
139
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
140
+ const v = raw;
141
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
142
+ const path2 = v["_path"];
143
+ const sse = v["_sse"];
144
+ if (method !== "SSE") return null;
145
+ if (typeof path2 !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
146
+ return null;
147
+ }
148
+ const s = sse;
149
+ const rawEvents = s["_events"];
150
+ if (!Array.isArray(rawEvents)) return null;
151
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
152
+ if (!events.length) return null;
153
+ return {
154
+ path: path2,
155
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
156
+ loop: s["_loop"] !== false,
157
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
158
+ events
159
+ };
160
+ }
161
+ function normaliseEvent(raw) {
162
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
163
+ const v = raw;
164
+ if (!("_data" in v)) return null;
165
+ const event = { data: v["_data"] };
166
+ if (typeof v["_event"] === "string") event.event = v["_event"];
167
+ return event;
168
+ }
169
+ var init_parseSseRoutes = __esm({
170
+ "src/storage/parseSseRoutes.ts"() {
171
+ "use strict";
172
+ init_esm_shims();
173
+ }
174
+ });
175
+
133
176
  // src/storage/parseSchema.ts
134
177
  function parseSchema(raw) {
135
178
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -218,6 +261,8 @@ function normaliseRoute(raw) {
218
261
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
219
262
  const v = raw;
220
263
  const method = v["_method"];
264
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
265
+ if ("_sse" in v) return null;
221
266
  const path2 = v["_path"];
222
267
  if (typeof method !== "string" || typeof path2 !== "string") return null;
223
268
  const route = { method, path: path2 };
@@ -287,6 +332,58 @@ var init_parseData = __esm({
287
332
  }
288
333
  });
289
334
 
335
+ // src/storage/resolveDirectives.ts
336
+ import { randomUUID as randomUUID2 } from "crypto";
337
+ function resolveDirectives(data) {
338
+ const registry = /* @__PURE__ */ new Map();
339
+ let modified = false;
340
+ for (const [collection, items] of Object.entries(data)) {
341
+ for (const item of items) {
342
+ for (const [field, value] of Object.entries(item)) {
343
+ if (typeof value !== "string") continue;
344
+ const m = UUID_GEN_RE.exec(value);
345
+ if (!m) continue;
346
+ const uuid = randomUUID2();
347
+ item[field] = uuid;
348
+ modified = true;
349
+ const alias = m[1];
350
+ if (alias) {
351
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
352
+ registry.get(collection).set(alias, uuid);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ for (const items of Object.values(data)) {
358
+ for (const item of items) {
359
+ for (const [field, value] of Object.entries(item)) {
360
+ if (typeof value !== "string") continue;
361
+ const m = FK_RE.exec(value);
362
+ if (!m) continue;
363
+ const col = m[1];
364
+ const alias = m[2];
365
+ const uuid = registry.get(col)?.get(alias);
366
+ if (uuid !== void 0) {
367
+ item[field] = uuid;
368
+ modified = true;
369
+ } else {
370
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
371
+ }
372
+ }
373
+ }
374
+ }
375
+ return modified;
376
+ }
377
+ var UUID_GEN_RE, FK_RE;
378
+ var init_resolveDirectives = __esm({
379
+ "src/storage/resolveDirectives.ts"() {
380
+ "use strict";
381
+ init_esm_shims();
382
+ UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
383
+ FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
384
+ }
385
+ });
386
+
290
387
  // src/storage/yrestStorage.ts
291
388
  var yrestStorage_exports = {};
292
389
  __export(yrestStorage_exports, {
@@ -294,7 +391,7 @@ __export(yrestStorage_exports, {
294
391
  });
295
392
  import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
296
393
  import { resolve, dirname as dirname2 } from "path";
297
- import { randomUUID as randomUUID2 } from "crypto";
394
+ import { randomUUID as randomUUID3 } from "crypto";
298
395
  import { parse as parse2, stringify as stringify2 } from "yaml";
299
396
  function serializeRelations(relations) {
300
397
  const out = {};
@@ -355,15 +452,17 @@ function createYrestStorage(filePath) {
355
452
  const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
356
453
  const relations = parseRelations(raw["_rel"]);
357
454
  const routes = parseRoutes(raw["_routes"]);
455
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
358
456
  const schema = parseSchema(raw["_schema"]);
359
457
  const dataBlock = hasDataBlock(raw);
360
458
  const data = parseData(raw);
459
+ const directivesModified = resolveDirectives(data);
361
460
  let snapshot = {
362
461
  data: deepCopyData(data),
363
462
  relations: { ...relations },
364
463
  savedAt: /* @__PURE__ */ new Date()
365
464
  };
366
- return {
465
+ const storage = {
367
466
  getData() {
368
467
  return data;
369
468
  },
@@ -376,6 +475,9 @@ function createYrestStorage(filePath) {
376
475
  getRoutes() {
377
476
  return routes;
378
477
  },
478
+ getSseRoutes() {
479
+ return sseRoutes;
480
+ },
379
481
  getCollection(name) {
380
482
  return data[name];
381
483
  },
@@ -391,7 +493,7 @@ function createYrestStorage(filePath) {
391
493
  } else {
392
494
  Object.assign(payload, data);
393
495
  }
394
- const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
496
+ const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID3()}.tmp`);
395
497
  writeFileSync(tmp, stringify2(payload), "utf8");
396
498
  renameSync(tmp, absPath);
397
499
  },
@@ -403,6 +505,7 @@ function createYrestStorage(filePath) {
403
505
  Object.assign(data, freshData);
404
506
  for (const key of Object.keys(relations)) delete relations[key];
405
507
  Object.assign(relations, freshRelations);
508
+ if (resolveDirectives(data)) storage.persist();
406
509
  },
407
510
  getSnapshot() {
408
511
  return snapshot;
@@ -423,6 +526,8 @@ function createYrestStorage(filePath) {
423
526
  this.persist();
424
527
  }
425
528
  };
529
+ if (directivesModified) storage.persist();
530
+ return storage;
426
531
  }
427
532
  var init_yrestStorage = __esm({
428
533
  "src/storage/yrestStorage.ts"() {
@@ -430,8 +535,10 @@ var init_yrestStorage = __esm({
430
535
  init_esm_shims();
431
536
  init_parseRelations();
432
537
  init_parseRoutes();
538
+ init_parseSseRoutes();
433
539
  init_parseSchema();
434
540
  init_parseData();
541
+ init_resolveDirectives();
435
542
  init_deepCopy();
436
543
  }
437
544
  });
@@ -484,6 +591,7 @@ function dedent(str) {
484
591
  // src/api/yrestServer.ts
485
592
  init_esm_shims();
486
593
  init_parseRelations();
594
+ init_parseSseRoutes();
487
595
  init_parseSchema();
488
596
  import { resolve as resolve2 } from "path";
489
597
 
@@ -596,6 +704,8 @@ var METHOD_COLOR = {
596
704
  PUT: "#d29922",
597
705
  PATCH: "#a371f7",
598
706
  DELETE: "#f85149",
707
+ SSE: "#0ea5e9",
708
+ WS: "#8b5cf6",
599
709
  fn: "#f0883e"
600
710
  };
601
711
  function escapeHtml(str) {
@@ -787,6 +897,30 @@ function handlersAccordion(handlers, routes, base) {
787
897
  </tbody></table>
788
898
  </details>`;
789
899
  }
900
+ function sseRoutesAccordion(routes, base) {
901
+ if (!routes.length) return "";
902
+ const rows = routes.map((r) => {
903
+ const tags = [
904
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
905
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
906
+ ];
907
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
908
+ if (r.repeat !== void 0)
909
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
910
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
911
+ return endpointRow("SSE", `${base}${r.path}`, desc);
912
+ });
913
+ return `
914
+ <details class="resource-card nested-card">
915
+ <summary>
916
+ <span class="resource-name">SSE streams</span>
917
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
918
+ </summary>
919
+ <table><tbody>
920
+ ${rows.join("")}
921
+ </tbody></table>
922
+ </details>`;
923
+ }
790
924
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
791
925
  const examples = [];
792
926
  const firstCol = collections[0];
@@ -1459,6 +1593,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1459
1593
  const collections = Object.keys(storage.getData());
1460
1594
  const relations = storage.getRelations();
1461
1595
  const customRoutes = storage.getRoutes();
1596
+ const sseRoutes = storage.getSseRoutes();
1462
1597
  const base = options.base;
1463
1598
  const host = `http://${options.host}:${options.port}`;
1464
1599
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1642,6 +1777,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1642
1777
  ${accordions}
1643
1778
  ${nestedRoutesAccordion(relations, base)}
1644
1779
  ${options.snapshot ? snapshotAccordion() : ""}
1780
+ ${sseRoutesAccordion(sseRoutes, base)}
1645
1781
  ${customRoutesAccordion(customRoutes, base, handlers)}
1646
1782
  ${handlersAccordion(handlers, customRoutes, base)}
1647
1783
  </div>
@@ -2794,6 +2930,80 @@ var SnapshotRouteCommand = class {
2794
2930
  }
2795
2931
  };
2796
2932
 
2933
+ // src/router/routes/sse.routes.ts
2934
+ init_esm_shims();
2935
+ var SSERouteCommand = class {
2936
+ constructor(storage, base) {
2937
+ this.storage = storage;
2938
+ this.base = base;
2939
+ }
2940
+ storage;
2941
+ base;
2942
+ register(server) {
2943
+ for (const route of this.storage.getSseRoutes()) {
2944
+ const url = `${this.base}${route.path}`;
2945
+ server.route({
2946
+ method: "GET",
2947
+ url,
2948
+ handler: async (req, reply) => {
2949
+ reply.hijack();
2950
+ const raw = reply.raw;
2951
+ raw.setHeader("Content-Type", "text/event-stream");
2952
+ raw.setHeader("Cache-Control", "no-cache");
2953
+ raw.setHeader("Connection", "keep-alive");
2954
+ raw.setHeader("Access-Control-Allow-Origin", "*");
2955
+ raw.flushHeaders();
2956
+ let alive = true;
2957
+ req.raw.on("close", () => {
2958
+ alive = false;
2959
+ });
2960
+ const keepAlive = setInterval(() => {
2961
+ if (alive) raw.write(": ping\n\n");
2962
+ }, 15e3);
2963
+ let frameIndex = 0;
2964
+ let cycleCount = 0;
2965
+ function sendNext() {
2966
+ if (!alive) {
2967
+ clearInterval(keepAlive);
2968
+ return;
2969
+ }
2970
+ const ev = route.events[frameIndex];
2971
+ const ctx = {
2972
+ params: req.params,
2973
+ query: req.query,
2974
+ body: null,
2975
+ headers: req.headers
2976
+ };
2977
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
2978
+ let chunk = "";
2979
+ if (ev.event) chunk += `event: ${ev.event}
2980
+ `;
2981
+ chunk += `data: ${JSON.stringify(data)}
2982
+
2983
+ `;
2984
+ raw.write(chunk);
2985
+ frameIndex++;
2986
+ if (frameIndex >= route.events.length) {
2987
+ frameIndex = 0;
2988
+ cycleCount++;
2989
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
2990
+ if (done) {
2991
+ clearInterval(keepAlive);
2992
+ raw.end();
2993
+ return;
2994
+ }
2995
+ }
2996
+ if (alive) {
2997
+ setTimeout(sendNext, route.interval);
2998
+ }
2999
+ }
3000
+ sendNext();
3001
+ }
3002
+ });
3003
+ }
3004
+ }
3005
+ };
3006
+
2797
3007
  // src/router/resource.router.ts
2798
3008
  function buildResourceRouteCommands(storage, options) {
2799
3009
  const commands = [];
@@ -2835,6 +3045,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
2835
3045
  new AboutRouteCommand(storage, options, handlers),
2836
3046
  new OpenApiRouteCommand(storage, options),
2837
3047
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3048
+ new SSERouteCommand(storage, options.base),
2838
3049
  new CustomRouteCommand(storage, options.base, handlers),
2839
3050
  ...buildResourceRouteCommands(storage, options)
2840
3051
  ];
@@ -2916,6 +3127,7 @@ function createInMemoryStorage(data) {
2916
3127
  const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2917
3128
  const relations = parseRelations(raw["_rel"]);
2918
3129
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3130
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
2919
3131
  const schema = parseSchema(raw["_schema"]);
2920
3132
  const collections = Object.fromEntries(
2921
3133
  Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
@@ -2930,6 +3142,7 @@ function createInMemoryStorage(data) {
2930
3142
  getRelations: () => relations,
2931
3143
  getSchema: () => schema,
2932
3144
  getRoutes: () => routes,
3145
+ getSseRoutes: () => sseRoutes,
2933
3146
  getCollection: (name) => collections[name],
2934
3147
  setCollection: (name, items) => {
2935
3148
  collections[name] = items;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "description": "YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.",
5
5
  "keywords": [
6
6
  "yrest",