@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/README.md +100 -1
- package/dist/cli/index.js +245 -40
- package/dist/cli/index.mjs +245 -40
- package/dist/index.d.mts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +217 -4
- package/dist/index.mjs +216 -3
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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-${
|
|
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 ${tags.join(" ")}`;
|
|
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