@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/README.md +170 -24
- package/dist/cli/index.js +230 -16
- package/dist/cli/index.mjs +230 -16
- package/dist/index.d.mts +110 -3
- package/dist/index.d.ts +110 -3
- package/dist/index.js +243 -11
- package/dist/index.mjs +243 -11
- package/package.json +6 -2
package/dist/cli/index.mjs
CHANGED
|
@@ -637,6 +637,8 @@ function normaliseRoute(raw) {
|
|
|
637
637
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
638
638
|
const v = raw;
|
|
639
639
|
const method = v["_method"];
|
|
640
|
+
if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
|
|
641
|
+
if ("_sse" in v) return null;
|
|
640
642
|
const path = v["_path"];
|
|
641
643
|
if (typeof method !== "string" || typeof path !== "string") return null;
|
|
642
644
|
const route = { method, path };
|
|
@@ -676,6 +678,43 @@ function normaliseScenario(raw) {
|
|
|
676
678
|
return { when, response };
|
|
677
679
|
}
|
|
678
680
|
|
|
681
|
+
// src/storage/parseSseRoutes.ts
|
|
682
|
+
function parseSseRoutes(raw) {
|
|
683
|
+
if (!Array.isArray(raw)) return [];
|
|
684
|
+
return raw.map(normaliseSseRoute).filter((r) => r !== null);
|
|
685
|
+
}
|
|
686
|
+
function normaliseSseRoute(raw) {
|
|
687
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
688
|
+
const v = raw;
|
|
689
|
+
const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
|
|
690
|
+
const path = v["_path"];
|
|
691
|
+
const sse = v["_sse"];
|
|
692
|
+
if (method !== "SSE") return null;
|
|
693
|
+
if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const s = sse;
|
|
697
|
+
const rawEvents = s["_events"];
|
|
698
|
+
if (!Array.isArray(rawEvents)) return null;
|
|
699
|
+
const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
|
|
700
|
+
if (!events.length) return null;
|
|
701
|
+
return {
|
|
702
|
+
path,
|
|
703
|
+
interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
|
|
704
|
+
loop: s["_loop"] !== false,
|
|
705
|
+
repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
|
|
706
|
+
events
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function normaliseEvent(raw) {
|
|
710
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
711
|
+
const v = raw;
|
|
712
|
+
if (!("_data" in v)) return null;
|
|
713
|
+
const event = { data: v["_data"] };
|
|
714
|
+
if (typeof v["_event"] === "string") event.event = v["_event"];
|
|
715
|
+
return event;
|
|
716
|
+
}
|
|
717
|
+
|
|
679
718
|
// src/storage/parseSchema.ts
|
|
680
719
|
function parseSchema(raw) {
|
|
681
720
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
@@ -690,6 +729,7 @@ function parseSchema(raw) {
|
|
|
690
729
|
}
|
|
691
730
|
return result;
|
|
692
731
|
}
|
|
732
|
+
var VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
|
|
693
733
|
function normaliseFieldDef(value) {
|
|
694
734
|
if (value === "required") return { required: true };
|
|
695
735
|
if (value === "optional") return { required: false };
|
|
@@ -697,15 +737,60 @@ function normaliseFieldDef(value) {
|
|
|
697
737
|
const v = value;
|
|
698
738
|
const def = {};
|
|
699
739
|
if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
|
|
700
|
-
if (typeof v["_type"] === "string" &&
|
|
740
|
+
if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
|
|
701
741
|
def.type = v["_type"];
|
|
702
742
|
if (typeof v["_format"] === "string") def.format = v["_format"];
|
|
703
743
|
if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
|
|
704
744
|
if (typeof v["_description"] === "string") def.description = v["_description"];
|
|
705
745
|
if (v["_default"] !== void 0) def.default = v["_default"];
|
|
746
|
+
if (v["_example"] !== void 0) def.example = v["_example"];
|
|
747
|
+
if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
|
|
748
|
+
if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
|
|
749
|
+
if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
|
|
750
|
+
if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
|
|
751
|
+
if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
|
|
752
|
+
if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
|
|
753
|
+
if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
|
|
754
|
+
if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
|
|
755
|
+
if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
|
|
756
|
+
if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
|
|
757
|
+
if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
|
|
758
|
+
if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
|
|
759
|
+
def.uniqueItems = v["_uniqueItems"];
|
|
760
|
+
if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
|
|
761
|
+
const items = v["_items"];
|
|
762
|
+
const itemsDef = {};
|
|
763
|
+
if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
|
|
764
|
+
itemsDef.type = items["_type"];
|
|
765
|
+
if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
|
|
766
|
+
if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
|
|
767
|
+
def.items = itemsDef;
|
|
768
|
+
}
|
|
769
|
+
if (v["_deprecated"] === true || v["_deprecated"] === false)
|
|
770
|
+
def.deprecated = v["_deprecated"];
|
|
771
|
+
if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
|
|
772
|
+
if (v["_writeOnly"] === true || v["_writeOnly"] === false)
|
|
773
|
+
def.writeOnly = v["_writeOnly"];
|
|
706
774
|
return def;
|
|
707
775
|
}
|
|
708
776
|
|
|
777
|
+
// src/storage/parseData.ts
|
|
778
|
+
var RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
|
|
779
|
+
function parseData(raw) {
|
|
780
|
+
const block = raw["_data"];
|
|
781
|
+
const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
|
|
782
|
+
Object.entries(block).filter(([, v]) => Array.isArray(v))
|
|
783
|
+
) : {};
|
|
784
|
+
const fromRoot = Object.fromEntries(
|
|
785
|
+
Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
|
|
786
|
+
);
|
|
787
|
+
return { ...fromBlock, ...fromRoot };
|
|
788
|
+
}
|
|
789
|
+
function hasDataBlock(raw) {
|
|
790
|
+
const d = raw["_data"];
|
|
791
|
+
return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
|
|
792
|
+
}
|
|
793
|
+
|
|
709
794
|
// src/utils/deepCopy.ts
|
|
710
795
|
function deepCopyData(source) {
|
|
711
796
|
return Object.fromEntries(
|
|
@@ -771,13 +856,12 @@ function serializeScenario(s) {
|
|
|
771
856
|
function createYrestStorage(filePath) {
|
|
772
857
|
const absPath = resolve2(filePath);
|
|
773
858
|
const raw = parse(readFileSync(absPath, "utf8")) ?? {};
|
|
774
|
-
const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
|
|
775
859
|
const relations = parseRelations(raw["_rel"]);
|
|
776
860
|
const routes = parseRoutes(raw["_routes"]);
|
|
861
|
+
const sseRoutes = parseSseRoutes(raw["_routes"]);
|
|
777
862
|
const schema = parseSchema(raw["_schema"]);
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
);
|
|
863
|
+
const dataBlock = hasDataBlock(raw);
|
|
864
|
+
const data = parseData(raw);
|
|
781
865
|
let snapshot = {
|
|
782
866
|
data: deepCopyData(data),
|
|
783
867
|
relations: { ...relations },
|
|
@@ -796,6 +880,9 @@ function createYrestStorage(filePath) {
|
|
|
796
880
|
getRoutes() {
|
|
797
881
|
return routes;
|
|
798
882
|
},
|
|
883
|
+
getSseRoutes() {
|
|
884
|
+
return sseRoutes;
|
|
885
|
+
},
|
|
799
886
|
getCollection(name) {
|
|
800
887
|
return data[name];
|
|
801
888
|
},
|
|
@@ -806,7 +893,11 @@ function createYrestStorage(filePath) {
|
|
|
806
893
|
const payload = {};
|
|
807
894
|
if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
|
|
808
895
|
if (routes.length > 0) payload._routes = serializeRoutes(routes);
|
|
809
|
-
|
|
896
|
+
if (dataBlock) {
|
|
897
|
+
payload._data = data;
|
|
898
|
+
} else {
|
|
899
|
+
Object.assign(payload, data);
|
|
900
|
+
}
|
|
810
901
|
const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID()}.tmp`);
|
|
811
902
|
writeFileSync2(tmp, stringify(payload), "utf8");
|
|
812
903
|
renameSync(tmp, absPath);
|
|
@@ -814,9 +905,7 @@ function createYrestStorage(filePath) {
|
|
|
814
905
|
reload() {
|
|
815
906
|
const fresh = parse(readFileSync(absPath, "utf8")) ?? {};
|
|
816
907
|
const freshRelations = parseRelations(fresh["_rel"]);
|
|
817
|
-
const freshData =
|
|
818
|
-
Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
|
|
819
|
-
);
|
|
908
|
+
const freshData = parseData(fresh);
|
|
820
909
|
for (const key of Object.keys(data)) delete data[key];
|
|
821
910
|
Object.assign(data, freshData);
|
|
822
911
|
for (const key of Object.keys(relations)) delete relations[key];
|
|
@@ -905,6 +994,8 @@ var METHOD_COLOR = {
|
|
|
905
994
|
PUT: "#d29922",
|
|
906
995
|
PATCH: "#a371f7",
|
|
907
996
|
DELETE: "#f85149",
|
|
997
|
+
SSE: "#0ea5e9",
|
|
998
|
+
WS: "#8b5cf6",
|
|
908
999
|
fn: "#f0883e"
|
|
909
1000
|
};
|
|
910
1001
|
function escapeHtml(str) {
|
|
@@ -1096,6 +1187,30 @@ function handlersAccordion(handlers, routes, base) {
|
|
|
1096
1187
|
</tbody></table>
|
|
1097
1188
|
</details>`;
|
|
1098
1189
|
}
|
|
1190
|
+
function sseRoutesAccordion(routes, base) {
|
|
1191
|
+
if (!routes.length) return "";
|
|
1192
|
+
const rows = routes.map((r) => {
|
|
1193
|
+
const tags = [
|
|
1194
|
+
`<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
|
|
1195
|
+
`<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
|
|
1196
|
+
];
|
|
1197
|
+
if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
|
|
1198
|
+
if (r.repeat !== void 0)
|
|
1199
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
|
|
1200
|
+
const desc = `SSE stream ${tags.join(" ")}`;
|
|
1201
|
+
return endpointRow("SSE", `${base}${r.path}`, desc);
|
|
1202
|
+
});
|
|
1203
|
+
return `
|
|
1204
|
+
<details class="resource-card nested-card">
|
|
1205
|
+
<summary>
|
|
1206
|
+
<span class="resource-name">SSE streams</span>
|
|
1207
|
+
<span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
|
|
1208
|
+
</summary>
|
|
1209
|
+
<table><tbody>
|
|
1210
|
+
${rows.join("")}
|
|
1211
|
+
</tbody></table>
|
|
1212
|
+
</details>`;
|
|
1213
|
+
}
|
|
1099
1214
|
function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
|
|
1100
1215
|
const examples = [];
|
|
1101
1216
|
const firstCol = collections[0];
|
|
@@ -1766,6 +1881,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
1766
1881
|
const collections = Object.keys(storage.getData());
|
|
1767
1882
|
const relations = storage.getRelations();
|
|
1768
1883
|
const customRoutes = storage.getRoutes();
|
|
1884
|
+
const sseRoutes = storage.getSseRoutes();
|
|
1769
1885
|
const base = options.base;
|
|
1770
1886
|
const host = `http://${options.host}:${options.port}`;
|
|
1771
1887
|
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
@@ -1949,6 +2065,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
1949
2065
|
${accordions}
|
|
1950
2066
|
${nestedRoutesAccordion(relations, base)}
|
|
1951
2067
|
${options.snapshot ? snapshotAccordion() : ""}
|
|
2068
|
+
${sseRoutesAccordion(sseRoutes, base)}
|
|
1952
2069
|
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
1953
2070
|
${handlersAccordion(handlers, customRoutes, base)}
|
|
1954
2071
|
</div>
|
|
@@ -2376,15 +2493,19 @@ function resolveRequestPath(dotPath, req) {
|
|
|
2376
2493
|
const [root, ...rest] = dotPath.split(".");
|
|
2377
2494
|
let value;
|
|
2378
2495
|
switch (root) {
|
|
2496
|
+
case "_body":
|
|
2379
2497
|
case "body":
|
|
2380
2498
|
value = req.body;
|
|
2381
2499
|
break;
|
|
2500
|
+
case "_params":
|
|
2382
2501
|
case "params":
|
|
2383
2502
|
value = req.params;
|
|
2384
2503
|
break;
|
|
2504
|
+
case "_query":
|
|
2385
2505
|
case "query":
|
|
2386
2506
|
value = req.query;
|
|
2387
2507
|
break;
|
|
2508
|
+
case "_headers":
|
|
2388
2509
|
case "headers":
|
|
2389
2510
|
value = req.headers;
|
|
2390
2511
|
break;
|
|
@@ -2676,6 +2797,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
|
|
|
2676
2797
|
if (def?.description) prop.description = def.description;
|
|
2677
2798
|
if (def?.enum) prop.enum = def.enum;
|
|
2678
2799
|
if (def?.default !== void 0) prop.default = def.default;
|
|
2800
|
+
if (def?.example !== void 0) prop.example = def.example;
|
|
2801
|
+
if (def?.nullable) prop.nullable = def.nullable;
|
|
2802
|
+
if (def?.minLength !== void 0) prop.minLength = def.minLength;
|
|
2803
|
+
if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
|
|
2804
|
+
if (def?.pattern) prop.pattern = def.pattern;
|
|
2805
|
+
if (def?.minimum !== void 0) prop.minimum = def.minimum;
|
|
2806
|
+
if (def?.maximum !== void 0) prop.maximum = def.maximum;
|
|
2807
|
+
if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
|
|
2808
|
+
if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
|
|
2809
|
+
if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
|
|
2810
|
+
if (def?.items) prop.items = def.items;
|
|
2811
|
+
if (def?.minItems !== void 0) prop.minItems = def.minItems;
|
|
2812
|
+
if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
|
|
2813
|
+
if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
|
|
2814
|
+
if (def?.deprecated) prop.deprecated = def.deprecated;
|
|
2815
|
+
if (def?.readOnly) prop.readOnly = def.readOnly;
|
|
2816
|
+
if (def?.writeOnly) prop.writeOnly = def.writeOnly;
|
|
2679
2817
|
properties[field] = prop;
|
|
2680
2818
|
if (def?.required === true) required.push(field);
|
|
2681
2819
|
}
|
|
@@ -3058,6 +3196,79 @@ var SnapshotRouteCommand = class {
|
|
|
3058
3196
|
}
|
|
3059
3197
|
};
|
|
3060
3198
|
|
|
3199
|
+
// src/router/routes/sse.routes.ts
|
|
3200
|
+
var SSERouteCommand = class {
|
|
3201
|
+
constructor(storage, base) {
|
|
3202
|
+
this.storage = storage;
|
|
3203
|
+
this.base = base;
|
|
3204
|
+
}
|
|
3205
|
+
storage;
|
|
3206
|
+
base;
|
|
3207
|
+
register(server) {
|
|
3208
|
+
for (const route of this.storage.getSseRoutes()) {
|
|
3209
|
+
const url = `${this.base}${route.path}`;
|
|
3210
|
+
server.route({
|
|
3211
|
+
method: "GET",
|
|
3212
|
+
url,
|
|
3213
|
+
handler: async (req, reply) => {
|
|
3214
|
+
reply.hijack();
|
|
3215
|
+
const raw = reply.raw;
|
|
3216
|
+
raw.setHeader("Content-Type", "text/event-stream");
|
|
3217
|
+
raw.setHeader("Cache-Control", "no-cache");
|
|
3218
|
+
raw.setHeader("Connection", "keep-alive");
|
|
3219
|
+
raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
3220
|
+
raw.flushHeaders();
|
|
3221
|
+
let alive = true;
|
|
3222
|
+
req.raw.on("close", () => {
|
|
3223
|
+
alive = false;
|
|
3224
|
+
});
|
|
3225
|
+
const keepAlive = setInterval(() => {
|
|
3226
|
+
if (alive) raw.write(": ping\n\n");
|
|
3227
|
+
}, 15e3);
|
|
3228
|
+
let frameIndex = 0;
|
|
3229
|
+
let cycleCount = 0;
|
|
3230
|
+
function sendNext() {
|
|
3231
|
+
if (!alive) {
|
|
3232
|
+
clearInterval(keepAlive);
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
const ev = route.events[frameIndex];
|
|
3236
|
+
const ctx = {
|
|
3237
|
+
params: req.params,
|
|
3238
|
+
query: req.query,
|
|
3239
|
+
body: null,
|
|
3240
|
+
headers: req.headers
|
|
3241
|
+
};
|
|
3242
|
+
const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
|
|
3243
|
+
let chunk = "";
|
|
3244
|
+
if (ev.event) chunk += `event: ${ev.event}
|
|
3245
|
+
`;
|
|
3246
|
+
chunk += `data: ${JSON.stringify(data)}
|
|
3247
|
+
|
|
3248
|
+
`;
|
|
3249
|
+
raw.write(chunk);
|
|
3250
|
+
frameIndex++;
|
|
3251
|
+
if (frameIndex >= route.events.length) {
|
|
3252
|
+
frameIndex = 0;
|
|
3253
|
+
cycleCount++;
|
|
3254
|
+
const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
|
|
3255
|
+
if (done) {
|
|
3256
|
+
clearInterval(keepAlive);
|
|
3257
|
+
raw.end();
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (alive) {
|
|
3262
|
+
setTimeout(sendNext, route.interval);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
sendNext();
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
|
|
3061
3272
|
// src/router/resource.router.ts
|
|
3062
3273
|
function buildResourceRouteCommands(storage, options) {
|
|
3063
3274
|
const commands = [];
|
|
@@ -3099,6 +3310,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
|
|
|
3099
3310
|
new AboutRouteCommand(storage, options, handlers),
|
|
3100
3311
|
new OpenApiRouteCommand(storage, options),
|
|
3101
3312
|
...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
|
|
3313
|
+
new SSERouteCommand(storage, options.base),
|
|
3102
3314
|
new CustomRouteCommand(storage, options.base, handlers),
|
|
3103
3315
|
...buildResourceRouteCommands(storage, options)
|
|
3104
3316
|
];
|
|
@@ -3218,6 +3430,14 @@ async function loadHandlers(filePath) {
|
|
|
3218
3430
|
}
|
|
3219
3431
|
|
|
3220
3432
|
// src/cli/commands/serve.ts
|
|
3433
|
+
function buildServeOptions(file, fileConfig, cliOverrides, fileArgIsExplicit = false) {
|
|
3434
|
+
return yrestOptionsSchema.parse({
|
|
3435
|
+
file,
|
|
3436
|
+
...fileConfig,
|
|
3437
|
+
...fileArgIsExplicit ? { file } : {},
|
|
3438
|
+
...cliOverrides
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3221
3441
|
function registerServe(program2) {
|
|
3222
3442
|
program2.command("serve").description("Start the mock server using a YAML file as database").argument("[file]", "Path to the YAML database file", "db.yml").option("-p, --port <number>", "Port to listen on", "3070").option("-H, --host <host>", "Host to bind", "localhost").option("-b, --base <path>", "Base path prefix for all routes", "").option("-w, --watch", "Reload db.yml automatically when it changes on disk").option("-r, --readonly", "Reject all write operations (POST, PUT, PATCH, DELETE) with 405").option(
|
|
3223
3443
|
"-d, --delay <ms>",
|
|
@@ -3241,13 +3461,7 @@ function registerServe(program2) {
|
|
|
3241
3461
|
const cliOverrides = Object.fromEntries(
|
|
3242
3462
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
3243
3463
|
);
|
|
3244
|
-
const
|
|
3245
|
-
file,
|
|
3246
|
-
...fileConfig,
|
|
3247
|
-
...cmd.args.length > 0 ? { file } : {},
|
|
3248
|
-
...cliOverrides
|
|
3249
|
-
};
|
|
3250
|
-
const options = yrestOptionsSchema.parse(merged);
|
|
3464
|
+
const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
|
|
3251
3465
|
let storage;
|
|
3252
3466
|
try {
|
|
3253
3467
|
storage = createYrestStorage(options.file);
|
package/dist/index.d.mts
CHANGED
|
@@ -10,21 +10,87 @@ type Resource = Record<string, unknown>;
|
|
|
10
10
|
* All properties are optional — omit what you don't need. Fields not mentioned
|
|
11
11
|
* in `_schema` are inferred from the collection data and treated as optional.
|
|
12
12
|
*
|
|
13
|
+
* In the YAML file every property uses the `_` prefix convention:
|
|
14
|
+
* `_type`, `_required`, `_format`, `_enum`, `_description`, `_default`, etc.
|
|
15
|
+
*
|
|
13
16
|
* Shorthand: `fieldName: required` normalises to `{ required: true }`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```yaml
|
|
20
|
+
* _schema:
|
|
21
|
+
* users:
|
|
22
|
+
* name:
|
|
23
|
+
* _type: string
|
|
24
|
+
* _required: true
|
|
25
|
+
* _minLength: 2
|
|
26
|
+
* _maxLength: 100
|
|
27
|
+
* email:
|
|
28
|
+
* _type: string
|
|
29
|
+
* _required: true
|
|
30
|
+
* _format: email
|
|
31
|
+
* age:
|
|
32
|
+
* _type: integer
|
|
33
|
+
* _minimum: 0
|
|
34
|
+
* _maximum: 120
|
|
35
|
+
* _nullable: true
|
|
36
|
+
* role:
|
|
37
|
+
* _type: string
|
|
38
|
+
* _enum: [admin, editor, viewer]
|
|
39
|
+
* _default: viewer
|
|
40
|
+
* tags:
|
|
41
|
+
* _type: array
|
|
42
|
+
* _items:
|
|
43
|
+
* _type: string
|
|
44
|
+
* _uniqueItems: true
|
|
45
|
+
* ```
|
|
14
46
|
*/
|
|
15
47
|
type FieldDef = {
|
|
16
|
-
/** If `true`, the field is listed in the OpenAPI `required` array
|
|
48
|
+
/** If `true`, the field is listed in the OpenAPI `required` array. */
|
|
17
49
|
required?: boolean;
|
|
18
50
|
/** Explicit type override. If absent, inferred from the data. */
|
|
19
51
|
type?: "string" | "integer" | "number" | "boolean" | "object" | "array";
|
|
20
|
-
/** OpenAPI `format` hint (e.g. `email`, `date`, `uuid`, `uri`, `date-time`). */
|
|
52
|
+
/** OpenAPI `format` hint (e.g. `email`, `date`, `uuid`, `uri`, `date-time`, `binary`). */
|
|
21
53
|
format?: string;
|
|
22
|
-
/** Restricts the field to a fixed set of values. */
|
|
54
|
+
/** Restricts the field to a fixed set of allowed values. */
|
|
23
55
|
enum?: unknown[];
|
|
24
56
|
/** Human-readable description included in the OpenAPI spec. */
|
|
25
57
|
description?: string;
|
|
26
58
|
/** Default value included in the OpenAPI spec. */
|
|
27
59
|
default?: unknown;
|
|
60
|
+
/** Example value included in the OpenAPI spec. */
|
|
61
|
+
example?: unknown;
|
|
62
|
+
/** If `true`, the field may be `null` in addition to its declared type. */
|
|
63
|
+
nullable?: boolean;
|
|
64
|
+
/** Minimum string length (inclusive). Applies when `type` is `string`. */
|
|
65
|
+
minLength?: number;
|
|
66
|
+
/** Maximum string length (inclusive). Applies when `type` is `string`. */
|
|
67
|
+
maxLength?: number;
|
|
68
|
+
/** Regex pattern the string value must match. Applies when `type` is `string`. */
|
|
69
|
+
pattern?: string;
|
|
70
|
+
/** Minimum numeric value (inclusive). Applies when `type` is `number` or `integer`. */
|
|
71
|
+
minimum?: number;
|
|
72
|
+
/** Maximum numeric value (inclusive). Applies when `type` is `number` or `integer`. */
|
|
73
|
+
maximum?: number;
|
|
74
|
+
/** Exclusive lower bound. Applies when `type` is `number` or `integer`. */
|
|
75
|
+
exclusiveMinimum?: number;
|
|
76
|
+
/** Exclusive upper bound. Applies when `type` is `number` or `integer`. */
|
|
77
|
+
exclusiveMaximum?: number;
|
|
78
|
+
/** Value must be a multiple of this number. */
|
|
79
|
+
multipleOf?: number;
|
|
80
|
+
/** Schema descriptor for the items of an array field. */
|
|
81
|
+
items?: Pick<FieldDef, "type" | "format" | "enum">;
|
|
82
|
+
/** Minimum number of items in the array. */
|
|
83
|
+
minItems?: number;
|
|
84
|
+
/** Maximum number of items in the array. */
|
|
85
|
+
maxItems?: number;
|
|
86
|
+
/** If `true`, all array items must be unique. */
|
|
87
|
+
uniqueItems?: boolean;
|
|
88
|
+
/** If `true`, marks the field as deprecated in the OpenAPI spec. */
|
|
89
|
+
deprecated?: boolean;
|
|
90
|
+
/** If `true`, the field is only returned in responses (excluded from request bodies). */
|
|
91
|
+
readOnly?: boolean;
|
|
92
|
+
/** If `true`, the field is only accepted in request bodies (excluded from responses). */
|
|
93
|
+
writeOnly?: boolean;
|
|
28
94
|
};
|
|
29
95
|
/**
|
|
30
96
|
* Field-level schema declarations for one or more collections, parsed from `_schema` in the YAML.
|
|
@@ -218,6 +284,42 @@ type CustomRoute = {
|
|
|
218
284
|
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
219
285
|
errorBody?: unknown;
|
|
220
286
|
};
|
|
287
|
+
/** A single event in an SSE stream declared under `_sse._events`. */
|
|
288
|
+
type SseEvent = {
|
|
289
|
+
/** SSE event name emitted as `event: <name>`. Omit for unnamed events. */
|
|
290
|
+
event?: string;
|
|
291
|
+
/** Payload serialised as JSON in the `data:` line. Supports `{{}}` templates. */
|
|
292
|
+
data: unknown;
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* A Server-Sent Events stream route declared under `_routes` with an `_sse` block.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```yaml
|
|
299
|
+
* _routes:
|
|
300
|
+
* - _method: GET
|
|
301
|
+
* _path: /events/orders
|
|
302
|
+
* _sse:
|
|
303
|
+
* _interval: 1500
|
|
304
|
+
* _loop: true
|
|
305
|
+
* _repeat: 5
|
|
306
|
+
* _events:
|
|
307
|
+
* - _event: update
|
|
308
|
+
* _data: { orderId: 1, status: processing }
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
type SseRoute = {
|
|
312
|
+
/** URL path. May include Fastify path params (e.g. `/events/:id`). */
|
|
313
|
+
path: string;
|
|
314
|
+
/** Milliseconds between frames. Default: `1000`. */
|
|
315
|
+
interval: number;
|
|
316
|
+
/** Restart the sequence after the last event. Default: `true`. */
|
|
317
|
+
loop: boolean;
|
|
318
|
+
/** Stop after this many full cycles. Omit for infinite. */
|
|
319
|
+
repeat?: number;
|
|
320
|
+
/** Ordered list of events to emit. */
|
|
321
|
+
events: SseEvent[];
|
|
322
|
+
};
|
|
221
323
|
/**
|
|
222
324
|
* In-memory store backed by a YAML file.
|
|
223
325
|
*
|
|
@@ -270,6 +372,11 @@ interface YrestStorage {
|
|
|
270
372
|
* Returns an empty array if no `_routes` block is present.
|
|
271
373
|
*/
|
|
272
374
|
getRoutes(): CustomRoute[];
|
|
375
|
+
/**
|
|
376
|
+
* Returns the SSE stream routes declared under `_routes` (entries with an `_sse` block).
|
|
377
|
+
* Returns an empty array if none are present.
|
|
378
|
+
*/
|
|
379
|
+
getSseRoutes(): SseRoute[];
|
|
273
380
|
/**
|
|
274
381
|
* Returns the saved snapshot: a frozen copy of `data` and `relations` taken
|
|
275
382
|
* at the last call to {@link saveSnapshot} (or at construction time).
|
package/dist/index.d.ts
CHANGED
|
@@ -10,21 +10,87 @@ type Resource = Record<string, unknown>;
|
|
|
10
10
|
* All properties are optional — omit what you don't need. Fields not mentioned
|
|
11
11
|
* in `_schema` are inferred from the collection data and treated as optional.
|
|
12
12
|
*
|
|
13
|
+
* In the YAML file every property uses the `_` prefix convention:
|
|
14
|
+
* `_type`, `_required`, `_format`, `_enum`, `_description`, `_default`, etc.
|
|
15
|
+
*
|
|
13
16
|
* Shorthand: `fieldName: required` normalises to `{ required: true }`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```yaml
|
|
20
|
+
* _schema:
|
|
21
|
+
* users:
|
|
22
|
+
* name:
|
|
23
|
+
* _type: string
|
|
24
|
+
* _required: true
|
|
25
|
+
* _minLength: 2
|
|
26
|
+
* _maxLength: 100
|
|
27
|
+
* email:
|
|
28
|
+
* _type: string
|
|
29
|
+
* _required: true
|
|
30
|
+
* _format: email
|
|
31
|
+
* age:
|
|
32
|
+
* _type: integer
|
|
33
|
+
* _minimum: 0
|
|
34
|
+
* _maximum: 120
|
|
35
|
+
* _nullable: true
|
|
36
|
+
* role:
|
|
37
|
+
* _type: string
|
|
38
|
+
* _enum: [admin, editor, viewer]
|
|
39
|
+
* _default: viewer
|
|
40
|
+
* tags:
|
|
41
|
+
* _type: array
|
|
42
|
+
* _items:
|
|
43
|
+
* _type: string
|
|
44
|
+
* _uniqueItems: true
|
|
45
|
+
* ```
|
|
14
46
|
*/
|
|
15
47
|
type FieldDef = {
|
|
16
|
-
/** If `true`, the field is listed in the OpenAPI `required` array
|
|
48
|
+
/** If `true`, the field is listed in the OpenAPI `required` array. */
|
|
17
49
|
required?: boolean;
|
|
18
50
|
/** Explicit type override. If absent, inferred from the data. */
|
|
19
51
|
type?: "string" | "integer" | "number" | "boolean" | "object" | "array";
|
|
20
|
-
/** OpenAPI `format` hint (e.g. `email`, `date`, `uuid`, `uri`, `date-time`). */
|
|
52
|
+
/** OpenAPI `format` hint (e.g. `email`, `date`, `uuid`, `uri`, `date-time`, `binary`). */
|
|
21
53
|
format?: string;
|
|
22
|
-
/** Restricts the field to a fixed set of values. */
|
|
54
|
+
/** Restricts the field to a fixed set of allowed values. */
|
|
23
55
|
enum?: unknown[];
|
|
24
56
|
/** Human-readable description included in the OpenAPI spec. */
|
|
25
57
|
description?: string;
|
|
26
58
|
/** Default value included in the OpenAPI spec. */
|
|
27
59
|
default?: unknown;
|
|
60
|
+
/** Example value included in the OpenAPI spec. */
|
|
61
|
+
example?: unknown;
|
|
62
|
+
/** If `true`, the field may be `null` in addition to its declared type. */
|
|
63
|
+
nullable?: boolean;
|
|
64
|
+
/** Minimum string length (inclusive). Applies when `type` is `string`. */
|
|
65
|
+
minLength?: number;
|
|
66
|
+
/** Maximum string length (inclusive). Applies when `type` is `string`. */
|
|
67
|
+
maxLength?: number;
|
|
68
|
+
/** Regex pattern the string value must match. Applies when `type` is `string`. */
|
|
69
|
+
pattern?: string;
|
|
70
|
+
/** Minimum numeric value (inclusive). Applies when `type` is `number` or `integer`. */
|
|
71
|
+
minimum?: number;
|
|
72
|
+
/** Maximum numeric value (inclusive). Applies when `type` is `number` or `integer`. */
|
|
73
|
+
maximum?: number;
|
|
74
|
+
/** Exclusive lower bound. Applies when `type` is `number` or `integer`. */
|
|
75
|
+
exclusiveMinimum?: number;
|
|
76
|
+
/** Exclusive upper bound. Applies when `type` is `number` or `integer`. */
|
|
77
|
+
exclusiveMaximum?: number;
|
|
78
|
+
/** Value must be a multiple of this number. */
|
|
79
|
+
multipleOf?: number;
|
|
80
|
+
/** Schema descriptor for the items of an array field. */
|
|
81
|
+
items?: Pick<FieldDef, "type" | "format" | "enum">;
|
|
82
|
+
/** Minimum number of items in the array. */
|
|
83
|
+
minItems?: number;
|
|
84
|
+
/** Maximum number of items in the array. */
|
|
85
|
+
maxItems?: number;
|
|
86
|
+
/** If `true`, all array items must be unique. */
|
|
87
|
+
uniqueItems?: boolean;
|
|
88
|
+
/** If `true`, marks the field as deprecated in the OpenAPI spec. */
|
|
89
|
+
deprecated?: boolean;
|
|
90
|
+
/** If `true`, the field is only returned in responses (excluded from request bodies). */
|
|
91
|
+
readOnly?: boolean;
|
|
92
|
+
/** If `true`, the field is only accepted in request bodies (excluded from responses). */
|
|
93
|
+
writeOnly?: boolean;
|
|
28
94
|
};
|
|
29
95
|
/**
|
|
30
96
|
* Field-level schema declarations for one or more collections, parsed from `_schema` in the YAML.
|
|
@@ -218,6 +284,42 @@ type CustomRoute = {
|
|
|
218
284
|
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
219
285
|
errorBody?: unknown;
|
|
220
286
|
};
|
|
287
|
+
/** A single event in an SSE stream declared under `_sse._events`. */
|
|
288
|
+
type SseEvent = {
|
|
289
|
+
/** SSE event name emitted as `event: <name>`. Omit for unnamed events. */
|
|
290
|
+
event?: string;
|
|
291
|
+
/** Payload serialised as JSON in the `data:` line. Supports `{{}}` templates. */
|
|
292
|
+
data: unknown;
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* A Server-Sent Events stream route declared under `_routes` with an `_sse` block.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```yaml
|
|
299
|
+
* _routes:
|
|
300
|
+
* - _method: GET
|
|
301
|
+
* _path: /events/orders
|
|
302
|
+
* _sse:
|
|
303
|
+
* _interval: 1500
|
|
304
|
+
* _loop: true
|
|
305
|
+
* _repeat: 5
|
|
306
|
+
* _events:
|
|
307
|
+
* - _event: update
|
|
308
|
+
* _data: { orderId: 1, status: processing }
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
type SseRoute = {
|
|
312
|
+
/** URL path. May include Fastify path params (e.g. `/events/:id`). */
|
|
313
|
+
path: string;
|
|
314
|
+
/** Milliseconds between frames. Default: `1000`. */
|
|
315
|
+
interval: number;
|
|
316
|
+
/** Restart the sequence after the last event. Default: `true`. */
|
|
317
|
+
loop: boolean;
|
|
318
|
+
/** Stop after this many full cycles. Omit for infinite. */
|
|
319
|
+
repeat?: number;
|
|
320
|
+
/** Ordered list of events to emit. */
|
|
321
|
+
events: SseEvent[];
|
|
322
|
+
};
|
|
221
323
|
/**
|
|
222
324
|
* In-memory store backed by a YAML file.
|
|
223
325
|
*
|
|
@@ -270,6 +372,11 @@ interface YrestStorage {
|
|
|
270
372
|
* Returns an empty array if no `_routes` block is present.
|
|
271
373
|
*/
|
|
272
374
|
getRoutes(): CustomRoute[];
|
|
375
|
+
/**
|
|
376
|
+
* Returns the SSE stream routes declared under `_routes` (entries with an `_sse` block).
|
|
377
|
+
* Returns an empty array if none are present.
|
|
378
|
+
*/
|
|
379
|
+
getSseRoutes(): SseRoute[];
|
|
273
380
|
/**
|
|
274
381
|
* Returns the saved snapshot: a frozen copy of `data` and `relations` taken
|
|
275
382
|
* at the last call to {@link saveSnapshot} (or at construction time).
|