@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.js CHANGED
@@ -153,6 +153,49 @@ var init_parseRelations = __esm({
153
153
  }
154
154
  });
155
155
 
156
+ // src/storage/parseSseRoutes.ts
157
+ function parseSseRoutes(raw) {
158
+ if (!Array.isArray(raw)) return [];
159
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
160
+ }
161
+ function normaliseSseRoute(raw) {
162
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
163
+ const v = raw;
164
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
165
+ const path = v["_path"];
166
+ const sse = v["_sse"];
167
+ if (method !== "SSE") return null;
168
+ if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
169
+ return null;
170
+ }
171
+ const s = sse;
172
+ const rawEvents = s["_events"];
173
+ if (!Array.isArray(rawEvents)) return null;
174
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
175
+ if (!events.length) return null;
176
+ return {
177
+ path,
178
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
179
+ loop: s["_loop"] !== false,
180
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
181
+ events
182
+ };
183
+ }
184
+ function normaliseEvent(raw) {
185
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
186
+ const v = raw;
187
+ if (!("_data" in v)) return null;
188
+ const event = { data: v["_data"] };
189
+ if (typeof v["_event"] === "string") event.event = v["_event"];
190
+ return event;
191
+ }
192
+ var init_parseSseRoutes = __esm({
193
+ "src/storage/parseSseRoutes.ts"() {
194
+ "use strict";
195
+ init_cjs_shims();
196
+ }
197
+ });
198
+
156
199
  // src/storage/parseSchema.ts
157
200
  function parseSchema(raw) {
158
201
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -241,6 +284,8 @@ function normaliseRoute(raw) {
241
284
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
242
285
  const v = raw;
243
286
  const method = v["_method"];
287
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
288
+ if ("_sse" in v) return null;
244
289
  const path = v["_path"];
245
290
  if (typeof method !== "string" || typeof path !== "string") return null;
246
291
  const route = { method, path };
@@ -310,6 +355,58 @@ var init_parseData = __esm({
310
355
  }
311
356
  });
312
357
 
358
+ // src/storage/resolveDirectives.ts
359
+ function resolveDirectives(data) {
360
+ const registry = /* @__PURE__ */ new Map();
361
+ let modified = false;
362
+ for (const [collection, items] of Object.entries(data)) {
363
+ for (const item of items) {
364
+ for (const [field, value] of Object.entries(item)) {
365
+ if (typeof value !== "string") continue;
366
+ const m = UUID_GEN_RE.exec(value);
367
+ if (!m) continue;
368
+ const uuid = (0, import_node_crypto2.randomUUID)();
369
+ item[field] = uuid;
370
+ modified = true;
371
+ const alias = m[1];
372
+ if (alias) {
373
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
374
+ registry.get(collection).set(alias, uuid);
375
+ }
376
+ }
377
+ }
378
+ }
379
+ for (const items of Object.values(data)) {
380
+ for (const item of items) {
381
+ for (const [field, value] of Object.entries(item)) {
382
+ if (typeof value !== "string") continue;
383
+ const m = FK_RE.exec(value);
384
+ if (!m) continue;
385
+ const col = m[1];
386
+ const alias = m[2];
387
+ const uuid = registry.get(col)?.get(alias);
388
+ if (uuid !== void 0) {
389
+ item[field] = uuid;
390
+ modified = true;
391
+ } else {
392
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ return modified;
398
+ }
399
+ var import_node_crypto2, UUID_GEN_RE, FK_RE;
400
+ var init_resolveDirectives = __esm({
401
+ "src/storage/resolveDirectives.ts"() {
402
+ "use strict";
403
+ init_cjs_shims();
404
+ import_node_crypto2 = require("crypto");
405
+ UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
406
+ FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
407
+ }
408
+ });
409
+
313
410
  // src/storage/yrestStorage.ts
314
411
  var yrestStorage_exports = {};
315
412
  __export(yrestStorage_exports, {
@@ -374,15 +471,17 @@ function createYrestStorage(filePath) {
374
471
  const raw = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
375
472
  const relations = parseRelations(raw["_rel"]);
376
473
  const routes = parseRoutes(raw["_routes"]);
474
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
377
475
  const schema = parseSchema(raw["_schema"]);
378
476
  const dataBlock = hasDataBlock(raw);
379
477
  const data = parseData(raw);
478
+ const directivesModified = resolveDirectives(data);
380
479
  let snapshot = {
381
480
  data: deepCopyData(data),
382
481
  relations: { ...relations },
383
482
  savedAt: /* @__PURE__ */ new Date()
384
483
  };
385
- return {
484
+ const storage = {
386
485
  getData() {
387
486
  return data;
388
487
  },
@@ -395,6 +494,9 @@ function createYrestStorage(filePath) {
395
494
  getRoutes() {
396
495
  return routes;
397
496
  },
497
+ getSseRoutes() {
498
+ return sseRoutes;
499
+ },
398
500
  getCollection(name) {
399
501
  return data[name];
400
502
  },
@@ -410,7 +512,7 @@ function createYrestStorage(filePath) {
410
512
  } else {
411
513
  Object.assign(payload, data);
412
514
  }
413
- const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
515
+ const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto3.randomUUID)()}.tmp`);
414
516
  (0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml3.stringify)(payload), "utf8");
415
517
  (0, import_node_fs3.renameSync)(tmp, absPath);
416
518
  },
@@ -422,6 +524,7 @@ function createYrestStorage(filePath) {
422
524
  Object.assign(data, freshData);
423
525
  for (const key of Object.keys(relations)) delete relations[key];
424
526
  Object.assign(relations, freshRelations);
527
+ if (resolveDirectives(data)) storage.persist();
425
528
  },
426
529
  getSnapshot() {
427
530
  return snapshot;
@@ -442,20 +545,24 @@ function createYrestStorage(filePath) {
442
545
  this.persist();
443
546
  }
444
547
  };
548
+ if (directivesModified) storage.persist();
549
+ return storage;
445
550
  }
446
- var import_node_fs3, import_node_path2, import_node_crypto2, import_yaml3;
551
+ var import_node_fs3, import_node_path2, import_node_crypto3, import_yaml3;
447
552
  var init_yrestStorage = __esm({
448
553
  "src/storage/yrestStorage.ts"() {
449
554
  "use strict";
450
555
  init_cjs_shims();
451
556
  import_node_fs3 = require("fs");
452
557
  import_node_path2 = require("path");
453
- import_node_crypto2 = require("crypto");
558
+ import_node_crypto3 = require("crypto");
454
559
  import_yaml3 = require("yaml");
455
560
  init_parseRelations();
456
561
  init_parseRoutes();
562
+ init_parseSseRoutes();
457
563
  init_parseSchema();
458
564
  init_parseData();
565
+ init_resolveDirectives();
459
566
  init_deepCopy();
460
567
  }
461
568
  });
@@ -519,6 +626,7 @@ function dedent(str) {
519
626
  init_cjs_shims();
520
627
  var import_node_path3 = require("path");
521
628
  init_parseRelations();
629
+ init_parseSseRoutes();
522
630
  init_parseSchema();
523
631
 
524
632
  // src/utils/handlers.ts
@@ -630,6 +738,8 @@ var METHOD_COLOR = {
630
738
  PUT: "#d29922",
631
739
  PATCH: "#a371f7",
632
740
  DELETE: "#f85149",
741
+ SSE: "#0ea5e9",
742
+ WS: "#8b5cf6",
633
743
  fn: "#f0883e"
634
744
  };
635
745
  function escapeHtml(str) {
@@ -821,6 +931,30 @@ function handlersAccordion(handlers, routes, base) {
821
931
  </tbody></table>
822
932
  </details>`;
823
933
  }
934
+ function sseRoutesAccordion(routes, base) {
935
+ if (!routes.length) return "";
936
+ const rows = routes.map((r) => {
937
+ const tags = [
938
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
939
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
940
+ ];
941
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
942
+ if (r.repeat !== void 0)
943
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
944
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
945
+ return endpointRow("SSE", `${base}${r.path}`, desc);
946
+ });
947
+ return `
948
+ <details class="resource-card nested-card">
949
+ <summary>
950
+ <span class="resource-name">SSE streams</span>
951
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
952
+ </summary>
953
+ <table><tbody>
954
+ ${rows.join("")}
955
+ </tbody></table>
956
+ </details>`;
957
+ }
824
958
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
825
959
  const examples = [];
826
960
  const firstCol = collections[0];
@@ -1493,6 +1627,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1493
1627
  const collections = Object.keys(storage.getData());
1494
1628
  const relations = storage.getRelations();
1495
1629
  const customRoutes = storage.getRoutes();
1630
+ const sseRoutes = storage.getSseRoutes();
1496
1631
  const base = options.base;
1497
1632
  const host = `http://${options.host}:${options.port}`;
1498
1633
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1676,6 +1811,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1676
1811
  ${accordions}
1677
1812
  ${nestedRoutesAccordion(relations, base)}
1678
1813
  ${options.snapshot ? snapshotAccordion() : ""}
1814
+ ${sseRoutesAccordion(sseRoutes, base)}
1679
1815
  ${customRoutesAccordion(customRoutes, base, handlers)}
1680
1816
  ${handlersAccordion(handlers, customRoutes, base)}
1681
1817
  </div>
@@ -2828,6 +2964,80 @@ var SnapshotRouteCommand = class {
2828
2964
  }
2829
2965
  };
2830
2966
 
2967
+ // src/router/routes/sse.routes.ts
2968
+ init_cjs_shims();
2969
+ var SSERouteCommand = class {
2970
+ constructor(storage, base) {
2971
+ this.storage = storage;
2972
+ this.base = base;
2973
+ }
2974
+ storage;
2975
+ base;
2976
+ register(server) {
2977
+ for (const route of this.storage.getSseRoutes()) {
2978
+ const url = `${this.base}${route.path}`;
2979
+ server.route({
2980
+ method: "GET",
2981
+ url,
2982
+ handler: async (req, reply) => {
2983
+ reply.hijack();
2984
+ const raw = reply.raw;
2985
+ raw.setHeader("Content-Type", "text/event-stream");
2986
+ raw.setHeader("Cache-Control", "no-cache");
2987
+ raw.setHeader("Connection", "keep-alive");
2988
+ raw.setHeader("Access-Control-Allow-Origin", "*");
2989
+ raw.flushHeaders();
2990
+ let alive = true;
2991
+ req.raw.on("close", () => {
2992
+ alive = false;
2993
+ });
2994
+ const keepAlive = setInterval(() => {
2995
+ if (alive) raw.write(": ping\n\n");
2996
+ }, 15e3);
2997
+ let frameIndex = 0;
2998
+ let cycleCount = 0;
2999
+ function sendNext() {
3000
+ if (!alive) {
3001
+ clearInterval(keepAlive);
3002
+ return;
3003
+ }
3004
+ const ev = route.events[frameIndex];
3005
+ const ctx = {
3006
+ params: req.params,
3007
+ query: req.query,
3008
+ body: null,
3009
+ headers: req.headers
3010
+ };
3011
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
3012
+ let chunk = "";
3013
+ if (ev.event) chunk += `event: ${ev.event}
3014
+ `;
3015
+ chunk += `data: ${JSON.stringify(data)}
3016
+
3017
+ `;
3018
+ raw.write(chunk);
3019
+ frameIndex++;
3020
+ if (frameIndex >= route.events.length) {
3021
+ frameIndex = 0;
3022
+ cycleCount++;
3023
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
3024
+ if (done) {
3025
+ clearInterval(keepAlive);
3026
+ raw.end();
3027
+ return;
3028
+ }
3029
+ }
3030
+ if (alive) {
3031
+ setTimeout(sendNext, route.interval);
3032
+ }
3033
+ }
3034
+ sendNext();
3035
+ }
3036
+ });
3037
+ }
3038
+ }
3039
+ };
3040
+
2831
3041
  // src/router/resource.router.ts
2832
3042
  function buildResourceRouteCommands(storage, options) {
2833
3043
  const commands = [];
@@ -2869,6 +3079,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
2869
3079
  new AboutRouteCommand(storage, options, handlers),
2870
3080
  new OpenApiRouteCommand(storage, options),
2871
3081
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3082
+ new SSERouteCommand(storage, options.base),
2872
3083
  new CustomRouteCommand(storage, options.base, handlers),
2873
3084
  ...buildResourceRouteCommands(storage, options)
2874
3085
  ];
@@ -2950,6 +3161,7 @@ function createInMemoryStorage(data) {
2950
3161
  const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2951
3162
  const relations = parseRelations(raw["_rel"]);
2952
3163
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3164
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
2953
3165
  const schema = parseSchema(raw["_schema"]);
2954
3166
  const collections = Object.fromEntries(
2955
3167
  Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
@@ -2964,6 +3176,7 @@ function createInMemoryStorage(data) {
2964
3176
  getRelations: () => relations,
2965
3177
  getSchema: () => schema,
2966
3178
  getRoutes: () => routes,
3179
+ getSseRoutes: () => sseRoutes,
2967
3180
  getCollection: (name) => collections[name],
2968
3181
  setCollection: (name, items) => {
2969
3182
  collections[name] = items;