@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.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 {};
@@ -151,18 +194,48 @@ function normaliseFieldDef(value) {
151
194
  const v = value;
152
195
  const def = {};
153
196
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
154
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
197
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
155
198
  def.type = v["_type"];
156
199
  if (typeof v["_format"] === "string") def.format = v["_format"];
157
200
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
158
201
  if (typeof v["_description"] === "string") def.description = v["_description"];
159
202
  if (v["_default"] !== void 0) def.default = v["_default"];
203
+ if (v["_example"] !== void 0) def.example = v["_example"];
204
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
205
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
206
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
207
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
208
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
209
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
210
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
211
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
212
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
213
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
214
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
215
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
216
+ def.uniqueItems = v["_uniqueItems"];
217
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
218
+ const items = v["_items"];
219
+ const itemsDef = {};
220
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
221
+ itemsDef.type = items["_type"];
222
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
223
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
224
+ def.items = itemsDef;
225
+ }
226
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
227
+ def.deprecated = v["_deprecated"];
228
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
229
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
230
+ def.writeOnly = v["_writeOnly"];
160
231
  return def;
161
232
  }
233
+ var VALID_TYPES;
162
234
  var init_parseSchema = __esm({
163
235
  "src/storage/parseSchema.ts"() {
164
236
  "use strict";
165
237
  init_esm_shims();
238
+ VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
166
239
  }
167
240
  });
168
241
 
@@ -188,6 +261,8 @@ function normaliseRoute(raw) {
188
261
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
189
262
  const v = raw;
190
263
  const method = v["_method"];
264
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
265
+ if ("_sse" in v) return null;
191
266
  const path2 = v["_path"];
192
267
  if (typeof method !== "string" || typeof path2 !== "string") return null;
193
268
  const route = { method, path: path2 };
@@ -233,6 +308,30 @@ var init_parseRoutes = __esm({
233
308
  }
234
309
  });
235
310
 
311
+ // src/storage/parseData.ts
312
+ function parseData(raw) {
313
+ const block = raw["_data"];
314
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
315
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
316
+ ) : {};
317
+ const fromRoot = Object.fromEntries(
318
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
319
+ );
320
+ return { ...fromBlock, ...fromRoot };
321
+ }
322
+ function hasDataBlock(raw) {
323
+ const d = raw["_data"];
324
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
325
+ }
326
+ var RESERVED;
327
+ var init_parseData = __esm({
328
+ "src/storage/parseData.ts"() {
329
+ "use strict";
330
+ init_esm_shims();
331
+ RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
332
+ }
333
+ });
334
+
236
335
  // src/storage/yrestStorage.ts
237
336
  var yrestStorage_exports = {};
238
337
  __export(yrestStorage_exports, {
@@ -299,13 +398,12 @@ function serializeScenario(s) {
299
398
  function createYrestStorage(filePath) {
300
399
  const absPath = resolve(filePath);
301
400
  const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
302
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
303
401
  const relations = parseRelations(raw["_rel"]);
304
402
  const routes = parseRoutes(raw["_routes"]);
403
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
305
404
  const schema = parseSchema(raw["_schema"]);
306
- const data = Object.fromEntries(
307
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
308
- );
405
+ const dataBlock = hasDataBlock(raw);
406
+ const data = parseData(raw);
309
407
  let snapshot = {
310
408
  data: deepCopyData(data),
311
409
  relations: { ...relations },
@@ -324,6 +422,9 @@ function createYrestStorage(filePath) {
324
422
  getRoutes() {
325
423
  return routes;
326
424
  },
425
+ getSseRoutes() {
426
+ return sseRoutes;
427
+ },
327
428
  getCollection(name) {
328
429
  return data[name];
329
430
  },
@@ -334,7 +435,11 @@ function createYrestStorage(filePath) {
334
435
  const payload = {};
335
436
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
336
437
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
337
- Object.assign(payload, data);
438
+ if (dataBlock) {
439
+ payload._data = data;
440
+ } else {
441
+ Object.assign(payload, data);
442
+ }
338
443
  const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
339
444
  writeFileSync(tmp, stringify2(payload), "utf8");
340
445
  renameSync(tmp, absPath);
@@ -342,9 +447,7 @@ function createYrestStorage(filePath) {
342
447
  reload() {
343
448
  const fresh = parse2(readFileSync2(absPath, "utf8")) ?? {};
344
449
  const freshRelations = parseRelations(fresh["_rel"]);
345
- const freshData = Object.fromEntries(
346
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
347
- );
450
+ const freshData = parseData(fresh);
348
451
  for (const key of Object.keys(data)) delete data[key];
349
452
  Object.assign(data, freshData);
350
453
  for (const key of Object.keys(relations)) delete relations[key];
@@ -376,7 +479,9 @@ var init_yrestStorage = __esm({
376
479
  init_esm_shims();
377
480
  init_parseRelations();
378
481
  init_parseRoutes();
482
+ init_parseSseRoutes();
379
483
  init_parseSchema();
484
+ init_parseData();
380
485
  init_deepCopy();
381
486
  }
382
487
  });
@@ -429,6 +534,7 @@ function dedent(str) {
429
534
  // src/api/yrestServer.ts
430
535
  init_esm_shims();
431
536
  init_parseRelations();
537
+ init_parseSseRoutes();
432
538
  init_parseSchema();
433
539
  import { resolve as resolve2 } from "path";
434
540
 
@@ -541,6 +647,8 @@ var METHOD_COLOR = {
541
647
  PUT: "#d29922",
542
648
  PATCH: "#a371f7",
543
649
  DELETE: "#f85149",
650
+ SSE: "#0ea5e9",
651
+ WS: "#8b5cf6",
544
652
  fn: "#f0883e"
545
653
  };
546
654
  function escapeHtml(str) {
@@ -732,6 +840,30 @@ function handlersAccordion(handlers, routes, base) {
732
840
  </tbody></table>
733
841
  </details>`;
734
842
  }
843
+ function sseRoutesAccordion(routes, base) {
844
+ if (!routes.length) return "";
845
+ const rows = routes.map((r) => {
846
+ const tags = [
847
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
848
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
849
+ ];
850
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
851
+ if (r.repeat !== void 0)
852
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
853
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
854
+ return endpointRow("SSE", `${base}${r.path}`, desc);
855
+ });
856
+ return `
857
+ <details class="resource-card nested-card">
858
+ <summary>
859
+ <span class="resource-name">SSE streams</span>
860
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
861
+ </summary>
862
+ <table><tbody>
863
+ ${rows.join("")}
864
+ </tbody></table>
865
+ </details>`;
866
+ }
735
867
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
736
868
  const examples = [];
737
869
  const firstCol = collections[0];
@@ -1404,6 +1536,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1404
1536
  const collections = Object.keys(storage.getData());
1405
1537
  const relations = storage.getRelations();
1406
1538
  const customRoutes = storage.getRoutes();
1539
+ const sseRoutes = storage.getSseRoutes();
1407
1540
  const base = options.base;
1408
1541
  const host = `http://${options.host}:${options.port}`;
1409
1542
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1587,6 +1720,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1587
1720
  ${accordions}
1588
1721
  ${nestedRoutesAccordion(relations, base)}
1589
1722
  ${options.snapshot ? snapshotAccordion() : ""}
1723
+ ${sseRoutesAccordion(sseRoutes, base)}
1590
1724
  ${customRoutesAccordion(customRoutes, base, handlers)}
1591
1725
  ${handlersAccordion(handlers, customRoutes, base)}
1592
1726
  </div>
@@ -2025,15 +2159,19 @@ function resolveRequestPath(dotPath, req) {
2025
2159
  const [root, ...rest] = dotPath.split(".");
2026
2160
  let value;
2027
2161
  switch (root) {
2162
+ case "_body":
2028
2163
  case "body":
2029
2164
  value = req.body;
2030
2165
  break;
2166
+ case "_params":
2031
2167
  case "params":
2032
2168
  value = req.params;
2033
2169
  break;
2170
+ case "_query":
2034
2171
  case "query":
2035
2172
  value = req.query;
2036
2173
  break;
2174
+ case "_headers":
2037
2175
  case "headers":
2038
2176
  value = req.headers;
2039
2177
  break;
@@ -2334,6 +2472,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2334
2472
  if (def?.description) prop.description = def.description;
2335
2473
  if (def?.enum) prop.enum = def.enum;
2336
2474
  if (def?.default !== void 0) prop.default = def.default;
2475
+ if (def?.example !== void 0) prop.example = def.example;
2476
+ if (def?.nullable) prop.nullable = def.nullable;
2477
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2478
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2479
+ if (def?.pattern) prop.pattern = def.pattern;
2480
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2481
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2482
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2483
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2484
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2485
+ if (def?.items) prop.items = def.items;
2486
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2487
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2488
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2489
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2490
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2491
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2337
2492
  properties[field] = prop;
2338
2493
  if (def?.required === true) required.push(field);
2339
2494
  }
@@ -2718,6 +2873,80 @@ var SnapshotRouteCommand = class {
2718
2873
  }
2719
2874
  };
2720
2875
 
2876
+ // src/router/routes/sse.routes.ts
2877
+ init_esm_shims();
2878
+ var SSERouteCommand = class {
2879
+ constructor(storage, base) {
2880
+ this.storage = storage;
2881
+ this.base = base;
2882
+ }
2883
+ storage;
2884
+ base;
2885
+ register(server) {
2886
+ for (const route of this.storage.getSseRoutes()) {
2887
+ const url = `${this.base}${route.path}`;
2888
+ server.route({
2889
+ method: "GET",
2890
+ url,
2891
+ handler: async (req, reply) => {
2892
+ reply.hijack();
2893
+ const raw = reply.raw;
2894
+ raw.setHeader("Content-Type", "text/event-stream");
2895
+ raw.setHeader("Cache-Control", "no-cache");
2896
+ raw.setHeader("Connection", "keep-alive");
2897
+ raw.setHeader("Access-Control-Allow-Origin", "*");
2898
+ raw.flushHeaders();
2899
+ let alive = true;
2900
+ req.raw.on("close", () => {
2901
+ alive = false;
2902
+ });
2903
+ const keepAlive = setInterval(() => {
2904
+ if (alive) raw.write(": ping\n\n");
2905
+ }, 15e3);
2906
+ let frameIndex = 0;
2907
+ let cycleCount = 0;
2908
+ function sendNext() {
2909
+ if (!alive) {
2910
+ clearInterval(keepAlive);
2911
+ return;
2912
+ }
2913
+ const ev = route.events[frameIndex];
2914
+ const ctx = {
2915
+ params: req.params,
2916
+ query: req.query,
2917
+ body: null,
2918
+ headers: req.headers
2919
+ };
2920
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
2921
+ let chunk = "";
2922
+ if (ev.event) chunk += `event: ${ev.event}
2923
+ `;
2924
+ chunk += `data: ${JSON.stringify(data)}
2925
+
2926
+ `;
2927
+ raw.write(chunk);
2928
+ frameIndex++;
2929
+ if (frameIndex >= route.events.length) {
2930
+ frameIndex = 0;
2931
+ cycleCount++;
2932
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
2933
+ if (done) {
2934
+ clearInterval(keepAlive);
2935
+ raw.end();
2936
+ return;
2937
+ }
2938
+ }
2939
+ if (alive) {
2940
+ setTimeout(sendNext, route.interval);
2941
+ }
2942
+ }
2943
+ sendNext();
2944
+ }
2945
+ });
2946
+ }
2947
+ }
2948
+ };
2949
+
2721
2950
  // src/router/resource.router.ts
2722
2951
  function buildResourceRouteCommands(storage, options) {
2723
2952
  const commands = [];
@@ -2759,6 +2988,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
2759
2988
  new AboutRouteCommand(storage, options, handlers),
2760
2989
  new OpenApiRouteCommand(storage, options),
2761
2990
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
2991
+ new SSERouteCommand(storage, options.base),
2762
2992
  new CustomRouteCommand(storage, options.base, handlers),
2763
2993
  ...buildResourceRouteCommands(storage, options)
2764
2994
  ];
@@ -2837,12 +3067,13 @@ function buildOptions(opts) {
2837
3067
  }
2838
3068
  function createInMemoryStorage(data) {
2839
3069
  const raw = data;
2840
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
3070
+ const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2841
3071
  const relations = parseRelations(raw["_rel"]);
2842
3072
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3073
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
2843
3074
  const schema = parseSchema(raw["_schema"]);
2844
3075
  const collections = Object.fromEntries(
2845
- Object.entries(raw).filter(([k]) => !RESERVED.has(k))
3076
+ Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
2846
3077
  );
2847
3078
  let snapshot = {
2848
3079
  data: deepCopyData(collections),
@@ -2854,6 +3085,7 @@ function createInMemoryStorage(data) {
2854
3085
  getRelations: () => relations,
2855
3086
  getSchema: () => schema,
2856
3087
  getRoutes: () => routes,
3088
+ getSseRoutes: () => sseRoutes,
2857
3089
  getCollection: (name) => collections[name],
2858
3090
  setCollection: (name, items) => {
2859
3091
  collections[name] = items;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.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",
@@ -60,12 +60,16 @@
60
60
  "dev": "tsup --watch",
61
61
  "test": "vitest",
62
62
  "test:run": "vitest run",
63
+ "test:unit": "vitest run tests/config tests/cli tests/storage tests/utils",
64
+ "test:integration": "vitest run tests/routes tests/openapi tests/security tests/api",
65
+ "test:e2e": "vitest run tests/e2e",
66
+ "test:preflight": "vitest run",
63
67
  "typecheck": "tsc --noEmit",
64
68
  "lint": "eslint src tests",
65
69
  "lint:fix": "eslint src tests --fix",
66
70
  "format": "prettier --write .",
67
71
  "format:check": "prettier --check .",
68
- "prepublishOnly": "npm run test:run && npm run build",
72
+ "prepublishOnly": "npm run build && npm run test:preflight",
69
73
  "version": "node scripts/update-version-badge.mjs && git add README.md"
70
74
  },
71
75
  "dependencies": {