@yrest/cli 0.11.1 → 0.12.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 {};
@@ -174,18 +217,48 @@ function normaliseFieldDef(value) {
174
217
  const v = value;
175
218
  const def = {};
176
219
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
177
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
220
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
178
221
  def.type = v["_type"];
179
222
  if (typeof v["_format"] === "string") def.format = v["_format"];
180
223
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
181
224
  if (typeof v["_description"] === "string") def.description = v["_description"];
182
225
  if (v["_default"] !== void 0) def.default = v["_default"];
226
+ if (v["_example"] !== void 0) def.example = v["_example"];
227
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
228
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
229
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
230
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
231
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
232
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
233
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
234
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
235
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
236
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
237
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
238
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
239
+ def.uniqueItems = v["_uniqueItems"];
240
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
241
+ const items = v["_items"];
242
+ const itemsDef = {};
243
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
244
+ itemsDef.type = items["_type"];
245
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
246
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
247
+ def.items = itemsDef;
248
+ }
249
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
250
+ def.deprecated = v["_deprecated"];
251
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
252
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
253
+ def.writeOnly = v["_writeOnly"];
183
254
  return def;
184
255
  }
256
+ var VALID_TYPES;
185
257
  var init_parseSchema = __esm({
186
258
  "src/storage/parseSchema.ts"() {
187
259
  "use strict";
188
260
  init_cjs_shims();
261
+ VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
189
262
  }
190
263
  });
191
264
 
@@ -211,6 +284,8 @@ function normaliseRoute(raw) {
211
284
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
212
285
  const v = raw;
213
286
  const method = v["_method"];
287
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
288
+ if ("_sse" in v) return null;
214
289
  const path = v["_path"];
215
290
  if (typeof method !== "string" || typeof path !== "string") return null;
216
291
  const route = { method, path };
@@ -256,6 +331,30 @@ var init_parseRoutes = __esm({
256
331
  }
257
332
  });
258
333
 
334
+ // src/storage/parseData.ts
335
+ function parseData(raw) {
336
+ const block = raw["_data"];
337
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
338
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
339
+ ) : {};
340
+ const fromRoot = Object.fromEntries(
341
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
342
+ );
343
+ return { ...fromBlock, ...fromRoot };
344
+ }
345
+ function hasDataBlock(raw) {
346
+ const d = raw["_data"];
347
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
348
+ }
349
+ var RESERVED;
350
+ var init_parseData = __esm({
351
+ "src/storage/parseData.ts"() {
352
+ "use strict";
353
+ init_cjs_shims();
354
+ RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
355
+ }
356
+ });
357
+
259
358
  // src/storage/yrestStorage.ts
260
359
  var yrestStorage_exports = {};
261
360
  __export(yrestStorage_exports, {
@@ -318,13 +417,12 @@ function serializeScenario(s) {
318
417
  function createYrestStorage(filePath) {
319
418
  const absPath = (0, import_node_path2.resolve)(filePath);
320
419
  const raw = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
321
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
322
420
  const relations = parseRelations(raw["_rel"]);
323
421
  const routes = parseRoutes(raw["_routes"]);
422
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
324
423
  const schema = parseSchema(raw["_schema"]);
325
- const data = Object.fromEntries(
326
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
327
- );
424
+ const dataBlock = hasDataBlock(raw);
425
+ const data = parseData(raw);
328
426
  let snapshot = {
329
427
  data: deepCopyData(data),
330
428
  relations: { ...relations },
@@ -343,6 +441,9 @@ function createYrestStorage(filePath) {
343
441
  getRoutes() {
344
442
  return routes;
345
443
  },
444
+ getSseRoutes() {
445
+ return sseRoutes;
446
+ },
346
447
  getCollection(name) {
347
448
  return data[name];
348
449
  },
@@ -353,7 +454,11 @@ function createYrestStorage(filePath) {
353
454
  const payload = {};
354
455
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
355
456
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
356
- Object.assign(payload, data);
457
+ if (dataBlock) {
458
+ payload._data = data;
459
+ } else {
460
+ Object.assign(payload, data);
461
+ }
357
462
  const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
358
463
  (0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml3.stringify)(payload), "utf8");
359
464
  (0, import_node_fs3.renameSync)(tmp, absPath);
@@ -361,9 +466,7 @@ function createYrestStorage(filePath) {
361
466
  reload() {
362
467
  const fresh = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
363
468
  const freshRelations = parseRelations(fresh["_rel"]);
364
- const freshData = Object.fromEntries(
365
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
366
- );
469
+ const freshData = parseData(fresh);
367
470
  for (const key of Object.keys(data)) delete data[key];
368
471
  Object.assign(data, freshData);
369
472
  for (const key of Object.keys(relations)) delete relations[key];
@@ -400,7 +503,9 @@ var init_yrestStorage = __esm({
400
503
  import_yaml3 = require("yaml");
401
504
  init_parseRelations();
402
505
  init_parseRoutes();
506
+ init_parseSseRoutes();
403
507
  init_parseSchema();
508
+ init_parseData();
404
509
  init_deepCopy();
405
510
  }
406
511
  });
@@ -464,6 +569,7 @@ function dedent(str) {
464
569
  init_cjs_shims();
465
570
  var import_node_path3 = require("path");
466
571
  init_parseRelations();
572
+ init_parseSseRoutes();
467
573
  init_parseSchema();
468
574
 
469
575
  // src/utils/handlers.ts
@@ -575,6 +681,8 @@ var METHOD_COLOR = {
575
681
  PUT: "#d29922",
576
682
  PATCH: "#a371f7",
577
683
  DELETE: "#f85149",
684
+ SSE: "#0ea5e9",
685
+ WS: "#8b5cf6",
578
686
  fn: "#f0883e"
579
687
  };
580
688
  function escapeHtml(str) {
@@ -766,6 +874,30 @@ function handlersAccordion(handlers, routes, base) {
766
874
  </tbody></table>
767
875
  </details>`;
768
876
  }
877
+ function sseRoutesAccordion(routes, base) {
878
+ if (!routes.length) return "";
879
+ const rows = routes.map((r) => {
880
+ const tags = [
881
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
882
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
883
+ ];
884
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
885
+ if (r.repeat !== void 0)
886
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
887
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
888
+ return endpointRow("SSE", `${base}${r.path}`, desc);
889
+ });
890
+ return `
891
+ <details class="resource-card nested-card">
892
+ <summary>
893
+ <span class="resource-name">SSE streams</span>
894
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
895
+ </summary>
896
+ <table><tbody>
897
+ ${rows.join("")}
898
+ </tbody></table>
899
+ </details>`;
900
+ }
769
901
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
770
902
  const examples = [];
771
903
  const firstCol = collections[0];
@@ -1438,6 +1570,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1438
1570
  const collections = Object.keys(storage.getData());
1439
1571
  const relations = storage.getRelations();
1440
1572
  const customRoutes = storage.getRoutes();
1573
+ const sseRoutes = storage.getSseRoutes();
1441
1574
  const base = options.base;
1442
1575
  const host = `http://${options.host}:${options.port}`;
1443
1576
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1621,6 +1754,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1621
1754
  ${accordions}
1622
1755
  ${nestedRoutesAccordion(relations, base)}
1623
1756
  ${options.snapshot ? snapshotAccordion() : ""}
1757
+ ${sseRoutesAccordion(sseRoutes, base)}
1624
1758
  ${customRoutesAccordion(customRoutes, base, handlers)}
1625
1759
  ${handlersAccordion(handlers, customRoutes, base)}
1626
1760
  </div>
@@ -2059,15 +2193,19 @@ function resolveRequestPath(dotPath, req) {
2059
2193
  const [root, ...rest] = dotPath.split(".");
2060
2194
  let value;
2061
2195
  switch (root) {
2196
+ case "_body":
2062
2197
  case "body":
2063
2198
  value = req.body;
2064
2199
  break;
2200
+ case "_params":
2065
2201
  case "params":
2066
2202
  value = req.params;
2067
2203
  break;
2204
+ case "_query":
2068
2205
  case "query":
2069
2206
  value = req.query;
2070
2207
  break;
2208
+ case "_headers":
2071
2209
  case "headers":
2072
2210
  value = req.headers;
2073
2211
  break;
@@ -2368,6 +2506,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2368
2506
  if (def?.description) prop.description = def.description;
2369
2507
  if (def?.enum) prop.enum = def.enum;
2370
2508
  if (def?.default !== void 0) prop.default = def.default;
2509
+ if (def?.example !== void 0) prop.example = def.example;
2510
+ if (def?.nullable) prop.nullable = def.nullable;
2511
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2512
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2513
+ if (def?.pattern) prop.pattern = def.pattern;
2514
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2515
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2516
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2517
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2518
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2519
+ if (def?.items) prop.items = def.items;
2520
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2521
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2522
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2523
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2524
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2525
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2371
2526
  properties[field] = prop;
2372
2527
  if (def?.required === true) required.push(field);
2373
2528
  }
@@ -2752,6 +2907,80 @@ var SnapshotRouteCommand = class {
2752
2907
  }
2753
2908
  };
2754
2909
 
2910
+ // src/router/routes/sse.routes.ts
2911
+ init_cjs_shims();
2912
+ var SSERouteCommand = class {
2913
+ constructor(storage, base) {
2914
+ this.storage = storage;
2915
+ this.base = base;
2916
+ }
2917
+ storage;
2918
+ base;
2919
+ register(server) {
2920
+ for (const route of this.storage.getSseRoutes()) {
2921
+ const url = `${this.base}${route.path}`;
2922
+ server.route({
2923
+ method: "GET",
2924
+ url,
2925
+ handler: async (req, reply) => {
2926
+ reply.hijack();
2927
+ const raw = reply.raw;
2928
+ raw.setHeader("Content-Type", "text/event-stream");
2929
+ raw.setHeader("Cache-Control", "no-cache");
2930
+ raw.setHeader("Connection", "keep-alive");
2931
+ raw.setHeader("Access-Control-Allow-Origin", "*");
2932
+ raw.flushHeaders();
2933
+ let alive = true;
2934
+ req.raw.on("close", () => {
2935
+ alive = false;
2936
+ });
2937
+ const keepAlive = setInterval(() => {
2938
+ if (alive) raw.write(": ping\n\n");
2939
+ }, 15e3);
2940
+ let frameIndex = 0;
2941
+ let cycleCount = 0;
2942
+ function sendNext() {
2943
+ if (!alive) {
2944
+ clearInterval(keepAlive);
2945
+ return;
2946
+ }
2947
+ const ev = route.events[frameIndex];
2948
+ const ctx = {
2949
+ params: req.params,
2950
+ query: req.query,
2951
+ body: null,
2952
+ headers: req.headers
2953
+ };
2954
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
2955
+ let chunk = "";
2956
+ if (ev.event) chunk += `event: ${ev.event}
2957
+ `;
2958
+ chunk += `data: ${JSON.stringify(data)}
2959
+
2960
+ `;
2961
+ raw.write(chunk);
2962
+ frameIndex++;
2963
+ if (frameIndex >= route.events.length) {
2964
+ frameIndex = 0;
2965
+ cycleCount++;
2966
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
2967
+ if (done) {
2968
+ clearInterval(keepAlive);
2969
+ raw.end();
2970
+ return;
2971
+ }
2972
+ }
2973
+ if (alive) {
2974
+ setTimeout(sendNext, route.interval);
2975
+ }
2976
+ }
2977
+ sendNext();
2978
+ }
2979
+ });
2980
+ }
2981
+ }
2982
+ };
2983
+
2755
2984
  // src/router/resource.router.ts
2756
2985
  function buildResourceRouteCommands(storage, options) {
2757
2986
  const commands = [];
@@ -2793,6 +3022,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
2793
3022
  new AboutRouteCommand(storage, options, handlers),
2794
3023
  new OpenApiRouteCommand(storage, options),
2795
3024
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3025
+ new SSERouteCommand(storage, options.base),
2796
3026
  new CustomRouteCommand(storage, options.base, handlers),
2797
3027
  ...buildResourceRouteCommands(storage, options)
2798
3028
  ];
@@ -2871,12 +3101,13 @@ function buildOptions(opts) {
2871
3101
  }
2872
3102
  function createInMemoryStorage(data) {
2873
3103
  const raw = data;
2874
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
3104
+ const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2875
3105
  const relations = parseRelations(raw["_rel"]);
2876
3106
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3107
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
2877
3108
  const schema = parseSchema(raw["_schema"]);
2878
3109
  const collections = Object.fromEntries(
2879
- Object.entries(raw).filter(([k]) => !RESERVED.has(k))
3110
+ Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
2880
3111
  );
2881
3112
  let snapshot = {
2882
3113
  data: deepCopyData(collections),
@@ -2888,6 +3119,7 @@ function createInMemoryStorage(data) {
2888
3119
  getRelations: () => relations,
2889
3120
  getSchema: () => schema,
2890
3121
  getRoutes: () => routes,
3122
+ getSseRoutes: () => sseRoutes,
2891
3123
  getCollection: (name) => collections[name],
2892
3124
  setCollection: (name, items) => {
2893
3125
  collections[name] = items;