@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/cli/index.mjs
CHANGED
|
@@ -88,6 +88,7 @@ categories:
|
|
|
88
88
|
// src/cli/commands/templates/relational.ts
|
|
89
89
|
var relationalTemplate = `# yrest relational sample \u2014 blog
|
|
90
90
|
# Demonstrates: many2one, one2one and many2many relationships
|
|
91
|
+
# Also shows __uuid_gen and __fk directives for UUID-based IDs
|
|
91
92
|
# Run: npx @yrest/cli serve db.yml
|
|
92
93
|
# Docs: GET http://localhost:3070/_about
|
|
93
94
|
|
|
@@ -112,33 +113,36 @@ _rel:
|
|
|
112
113
|
_type: one2one
|
|
113
114
|
_target: users
|
|
114
115
|
|
|
116
|
+
# __uuid_gen:alias generates a UUID and registers it under that alias.
|
|
117
|
+
# __fk.collection:alias resolves to the UUID registered in that collection.
|
|
118
|
+
# yRest rewrites this file on first run, replacing all directives with real values.
|
|
115
119
|
users:
|
|
116
|
-
- id:
|
|
120
|
+
- id: __uuid_gen:ana
|
|
117
121
|
name: Ana Garc\xEDa
|
|
118
122
|
email: ana@example.com
|
|
119
123
|
role: author
|
|
120
|
-
- id:
|
|
124
|
+
- id: __uuid_gen:luis
|
|
121
125
|
name: Luis Mart\xEDnez
|
|
122
126
|
email: luis@example.com
|
|
123
127
|
role: author
|
|
124
|
-
- id:
|
|
128
|
+
- id: __uuid_gen:sara
|
|
125
129
|
name: Sara L\xF3pez
|
|
126
130
|
email: sara@example.com
|
|
127
131
|
role: reader
|
|
128
132
|
|
|
129
133
|
profiles:
|
|
130
|
-
- id:
|
|
131
|
-
userId:
|
|
134
|
+
- id: __uuid_gen
|
|
135
|
+
userId: __fk.users:ana
|
|
132
136
|
bio: Full-stack developer and open-source enthusiast
|
|
133
137
|
avatar: https://i.pravatar.cc/150?img=1
|
|
134
138
|
website: https://ana.dev
|
|
135
|
-
- id:
|
|
136
|
-
userId:
|
|
139
|
+
- id: __uuid_gen
|
|
140
|
+
userId: __fk.users:luis
|
|
137
141
|
bio: Backend engineer, coffee addict
|
|
138
142
|
avatar: https://i.pravatar.cc/150?img=2
|
|
139
143
|
website: https://luisdev.io
|
|
140
|
-
- id:
|
|
141
|
-
userId:
|
|
144
|
+
- id: __uuid_gen
|
|
145
|
+
userId: __fk.users:sara
|
|
142
146
|
bio: Designer turned frontend developer
|
|
143
147
|
avatar: https://i.pravatar.cc/150?img=3
|
|
144
148
|
website: null
|
|
@@ -161,75 +165,75 @@ tags:
|
|
|
161
165
|
color: "#ef4444"
|
|
162
166
|
|
|
163
167
|
posts:
|
|
164
|
-
- id:
|
|
168
|
+
- id: __uuid_gen:post-ts
|
|
165
169
|
title: Getting started with TypeScript
|
|
166
170
|
slug: getting-started-typescript
|
|
167
171
|
body: TypeScript adds static typing to JavaScript, catching errors at compile time...
|
|
168
|
-
userId:
|
|
172
|
+
userId: __fk.users:ana
|
|
169
173
|
published: true
|
|
170
174
|
views: 1420
|
|
171
175
|
createdAt: "2024-11-01"
|
|
172
|
-
- id:
|
|
176
|
+
- id: __uuid_gen:post-fastify
|
|
173
177
|
title: Building REST APIs with Fastify
|
|
174
178
|
slug: rest-apis-fastify
|
|
175
179
|
body: Fastify is the fastest Node.js web framework, perfect for building APIs...
|
|
176
|
-
userId:
|
|
180
|
+
userId: __fk.users:ana
|
|
177
181
|
published: true
|
|
178
182
|
views: 980
|
|
179
183
|
createdAt: "2024-11-15"
|
|
180
|
-
- id:
|
|
184
|
+
- id: __uuid_gen:post-testing
|
|
181
185
|
title: Testing strategies for modern apps
|
|
182
186
|
slug: testing-strategies-modern
|
|
183
187
|
body: A solid test strategy covers unit, integration and end-to-end scenarios...
|
|
184
|
-
userId:
|
|
188
|
+
userId: __fk.users:luis
|
|
185
189
|
published: true
|
|
186
190
|
views: 640
|
|
187
191
|
createdAt: "2024-12-03"
|
|
188
|
-
- id:
|
|
192
|
+
- id: __uuid_gen:post-yaml
|
|
189
193
|
title: YAML as a database format
|
|
190
194
|
slug: yaml-database-format
|
|
191
195
|
body: YAML is human-readable and expressive enough for mock data during development...
|
|
192
|
-
userId:
|
|
196
|
+
userId: __fk.users:luis
|
|
193
197
|
published: false
|
|
194
198
|
views: 0
|
|
195
199
|
createdAt: "2025-01-10"
|
|
196
200
|
|
|
197
201
|
# pivot table for posts \u2194 tags (many2many)
|
|
198
202
|
post_tags:
|
|
199
|
-
- { id: 1, postId:
|
|
200
|
-
- { id: 2, postId:
|
|
201
|
-
- { id: 3, postId:
|
|
202
|
-
- { id: 4, postId:
|
|
203
|
-
- { id: 5, postId:
|
|
204
|
-
- { id: 6, postId:
|
|
205
|
-
- { id: 7, postId:
|
|
206
|
-
- { id: 8, postId:
|
|
203
|
+
- { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
|
|
204
|
+
- { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
|
|
205
|
+
- { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
|
|
206
|
+
- { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
|
|
207
|
+
- { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
|
|
208
|
+
- { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
|
|
209
|
+
- { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
|
|
210
|
+
- { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
|
|
207
211
|
|
|
208
212
|
comments:
|
|
209
213
|
- id: 1
|
|
210
214
|
body: Great introduction! This helped me a lot.
|
|
211
|
-
postId:
|
|
212
|
-
userId:
|
|
215
|
+
postId: __fk.posts:post-ts
|
|
216
|
+
userId: __fk.users:sara
|
|
213
217
|
likes: 5
|
|
214
218
|
- id: 2
|
|
215
219
|
body: Could you cover generics in a follow-up post?
|
|
216
|
-
postId:
|
|
217
|
-
userId:
|
|
220
|
+
postId: __fk.posts:post-ts
|
|
221
|
+
userId: __fk.users:luis
|
|
218
222
|
likes: 3
|
|
219
223
|
- id: 3
|
|
220
224
|
body: Fastify is indeed much faster than Express in my benchmarks.
|
|
221
|
-
postId:
|
|
222
|
-
userId:
|
|
225
|
+
postId: __fk.posts:post-fastify
|
|
226
|
+
userId: __fk.users:sara
|
|
223
227
|
likes: 8
|
|
224
228
|
- id: 4
|
|
225
229
|
body: Do you have a GitHub repo with these examples?
|
|
226
|
-
postId:
|
|
227
|
-
userId:
|
|
230
|
+
postId: __fk.posts:post-fastify
|
|
231
|
+
userId: __fk.users:ana
|
|
228
232
|
likes: 1
|
|
229
233
|
- id: 5
|
|
230
234
|
body: E2E tests are underrated. Solid post!
|
|
231
|
-
postId:
|
|
232
|
-
userId:
|
|
235
|
+
postId: __fk.posts:post-testing
|
|
236
|
+
userId: __fk.users:ana
|
|
233
237
|
likes: 4
|
|
234
238
|
|
|
235
239
|
# Try these queries:
|
|
@@ -519,7 +523,7 @@ import { join as join2, resolve as resolve3 } from "path";
|
|
|
519
523
|
// src/storage/yrestStorage.ts
|
|
520
524
|
import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
521
525
|
import { resolve as resolve2, dirname } from "path";
|
|
522
|
-
import { randomUUID } from "crypto";
|
|
526
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
523
527
|
import { parse, stringify } from "yaml";
|
|
524
528
|
|
|
525
529
|
// src/storage/parseRelations.ts
|
|
@@ -637,6 +641,8 @@ function normaliseRoute(raw) {
|
|
|
637
641
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
638
642
|
const v = raw;
|
|
639
643
|
const method = v["_method"];
|
|
644
|
+
if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
|
|
645
|
+
if ("_sse" in v) return null;
|
|
640
646
|
const path = v["_path"];
|
|
641
647
|
if (typeof method !== "string" || typeof path !== "string") return null;
|
|
642
648
|
const route = { method, path };
|
|
@@ -676,6 +682,43 @@ function normaliseScenario(raw) {
|
|
|
676
682
|
return { when, response };
|
|
677
683
|
}
|
|
678
684
|
|
|
685
|
+
// src/storage/parseSseRoutes.ts
|
|
686
|
+
function parseSseRoutes(raw) {
|
|
687
|
+
if (!Array.isArray(raw)) return [];
|
|
688
|
+
return raw.map(normaliseSseRoute).filter((r) => r !== null);
|
|
689
|
+
}
|
|
690
|
+
function normaliseSseRoute(raw) {
|
|
691
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
692
|
+
const v = raw;
|
|
693
|
+
const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
|
|
694
|
+
const path = v["_path"];
|
|
695
|
+
const sse = v["_sse"];
|
|
696
|
+
if (method !== "SSE") return null;
|
|
697
|
+
if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
const s = sse;
|
|
701
|
+
const rawEvents = s["_events"];
|
|
702
|
+
if (!Array.isArray(rawEvents)) return null;
|
|
703
|
+
const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
|
|
704
|
+
if (!events.length) return null;
|
|
705
|
+
return {
|
|
706
|
+
path,
|
|
707
|
+
interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
|
|
708
|
+
loop: s["_loop"] !== false,
|
|
709
|
+
repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
|
|
710
|
+
events
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function normaliseEvent(raw) {
|
|
714
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
715
|
+
const v = raw;
|
|
716
|
+
if (!("_data" in v)) return null;
|
|
717
|
+
const event = { data: v["_data"] };
|
|
718
|
+
if (typeof v["_event"] === "string") event.event = v["_event"];
|
|
719
|
+
return event;
|
|
720
|
+
}
|
|
721
|
+
|
|
679
722
|
// src/storage/parseSchema.ts
|
|
680
723
|
function parseSchema(raw) {
|
|
681
724
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
@@ -752,6 +795,51 @@ function hasDataBlock(raw) {
|
|
|
752
795
|
return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
|
|
753
796
|
}
|
|
754
797
|
|
|
798
|
+
// src/storage/resolveDirectives.ts
|
|
799
|
+
import { randomUUID } from "crypto";
|
|
800
|
+
var UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
|
|
801
|
+
var FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
|
|
802
|
+
function resolveDirectives(data) {
|
|
803
|
+
const registry = /* @__PURE__ */ new Map();
|
|
804
|
+
let modified = false;
|
|
805
|
+
for (const [collection, items] of Object.entries(data)) {
|
|
806
|
+
for (const item of items) {
|
|
807
|
+
for (const [field, value] of Object.entries(item)) {
|
|
808
|
+
if (typeof value !== "string") continue;
|
|
809
|
+
const m = UUID_GEN_RE.exec(value);
|
|
810
|
+
if (!m) continue;
|
|
811
|
+
const uuid = randomUUID();
|
|
812
|
+
item[field] = uuid;
|
|
813
|
+
modified = true;
|
|
814
|
+
const alias = m[1];
|
|
815
|
+
if (alias) {
|
|
816
|
+
if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
|
|
817
|
+
registry.get(collection).set(alias, uuid);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
for (const items of Object.values(data)) {
|
|
823
|
+
for (const item of items) {
|
|
824
|
+
for (const [field, value] of Object.entries(item)) {
|
|
825
|
+
if (typeof value !== "string") continue;
|
|
826
|
+
const m = FK_RE.exec(value);
|
|
827
|
+
if (!m) continue;
|
|
828
|
+
const col = m[1];
|
|
829
|
+
const alias = m[2];
|
|
830
|
+
const uuid = registry.get(col)?.get(alias);
|
|
831
|
+
if (uuid !== void 0) {
|
|
832
|
+
item[field] = uuid;
|
|
833
|
+
modified = true;
|
|
834
|
+
} else {
|
|
835
|
+
console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return modified;
|
|
841
|
+
}
|
|
842
|
+
|
|
755
843
|
// src/utils/deepCopy.ts
|
|
756
844
|
function deepCopyData(source) {
|
|
757
845
|
return Object.fromEntries(
|
|
@@ -819,15 +907,17 @@ function createYrestStorage(filePath) {
|
|
|
819
907
|
const raw = parse(readFileSync(absPath, "utf8")) ?? {};
|
|
820
908
|
const relations = parseRelations(raw["_rel"]);
|
|
821
909
|
const routes = parseRoutes(raw["_routes"]);
|
|
910
|
+
const sseRoutes = parseSseRoutes(raw["_routes"]);
|
|
822
911
|
const schema = parseSchema(raw["_schema"]);
|
|
823
912
|
const dataBlock = hasDataBlock(raw);
|
|
824
913
|
const data = parseData(raw);
|
|
914
|
+
const directivesModified = resolveDirectives(data);
|
|
825
915
|
let snapshot = {
|
|
826
916
|
data: deepCopyData(data),
|
|
827
917
|
relations: { ...relations },
|
|
828
918
|
savedAt: /* @__PURE__ */ new Date()
|
|
829
919
|
};
|
|
830
|
-
|
|
920
|
+
const storage = {
|
|
831
921
|
getData() {
|
|
832
922
|
return data;
|
|
833
923
|
},
|
|
@@ -840,6 +930,9 @@ function createYrestStorage(filePath) {
|
|
|
840
930
|
getRoutes() {
|
|
841
931
|
return routes;
|
|
842
932
|
},
|
|
933
|
+
getSseRoutes() {
|
|
934
|
+
return sseRoutes;
|
|
935
|
+
},
|
|
843
936
|
getCollection(name) {
|
|
844
937
|
return data[name];
|
|
845
938
|
},
|
|
@@ -855,7 +948,7 @@ function createYrestStorage(filePath) {
|
|
|
855
948
|
} else {
|
|
856
949
|
Object.assign(payload, data);
|
|
857
950
|
}
|
|
858
|
-
const tmp = resolve2(dirname(absPath), `.yrest-${
|
|
951
|
+
const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID2()}.tmp`);
|
|
859
952
|
writeFileSync2(tmp, stringify(payload), "utf8");
|
|
860
953
|
renameSync(tmp, absPath);
|
|
861
954
|
},
|
|
@@ -867,6 +960,7 @@ function createYrestStorage(filePath) {
|
|
|
867
960
|
Object.assign(data, freshData);
|
|
868
961
|
for (const key of Object.keys(relations)) delete relations[key];
|
|
869
962
|
Object.assign(relations, freshRelations);
|
|
963
|
+
if (resolveDirectives(data)) storage.persist();
|
|
870
964
|
},
|
|
871
965
|
getSnapshot() {
|
|
872
966
|
return snapshot;
|
|
@@ -887,6 +981,8 @@ function createYrestStorage(filePath) {
|
|
|
887
981
|
this.persist();
|
|
888
982
|
}
|
|
889
983
|
};
|
|
984
|
+
if (directivesModified) storage.persist();
|
|
985
|
+
return storage;
|
|
890
986
|
}
|
|
891
987
|
|
|
892
988
|
// src/server/createServer.ts
|
|
@@ -899,7 +995,7 @@ import { dirname as dirname2, join } from "path";
|
|
|
899
995
|
import { fileURLToPath } from "url";
|
|
900
996
|
|
|
901
997
|
// src/utils/interpolate.ts
|
|
902
|
-
import { randomUUID as
|
|
998
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
903
999
|
function getPath(obj, path) {
|
|
904
1000
|
return path.split(".").reduce((acc, key) => {
|
|
905
1001
|
if (acc != null && typeof acc === "object") return acc[key];
|
|
@@ -908,7 +1004,7 @@ function getPath(obj, path) {
|
|
|
908
1004
|
}
|
|
909
1005
|
function resolveVar(path, ctx) {
|
|
910
1006
|
if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
|
|
911
|
-
if (path === "uuid") return
|
|
1007
|
+
if (path === "uuid") return randomUUID3();
|
|
912
1008
|
if (path === "body") return ctx.body;
|
|
913
1009
|
if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
|
|
914
1010
|
if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
|
|
@@ -951,6 +1047,8 @@ var METHOD_COLOR = {
|
|
|
951
1047
|
PUT: "#d29922",
|
|
952
1048
|
PATCH: "#a371f7",
|
|
953
1049
|
DELETE: "#f85149",
|
|
1050
|
+
SSE: "#0ea5e9",
|
|
1051
|
+
WS: "#8b5cf6",
|
|
954
1052
|
fn: "#f0883e"
|
|
955
1053
|
};
|
|
956
1054
|
function escapeHtml(str) {
|
|
@@ -1142,6 +1240,30 @@ function handlersAccordion(handlers, routes, base) {
|
|
|
1142
1240
|
</tbody></table>
|
|
1143
1241
|
</details>`;
|
|
1144
1242
|
}
|
|
1243
|
+
function sseRoutesAccordion(routes, base) {
|
|
1244
|
+
if (!routes.length) return "";
|
|
1245
|
+
const rows = routes.map((r) => {
|
|
1246
|
+
const tags = [
|
|
1247
|
+
`<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
|
|
1248
|
+
`<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
|
|
1249
|
+
];
|
|
1250
|
+
if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
|
|
1251
|
+
if (r.repeat !== void 0)
|
|
1252
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
|
|
1253
|
+
const desc = `SSE stream ${tags.join(" ")}`;
|
|
1254
|
+
return endpointRow("SSE", `${base}${r.path}`, desc);
|
|
1255
|
+
});
|
|
1256
|
+
return `
|
|
1257
|
+
<details class="resource-card nested-card">
|
|
1258
|
+
<summary>
|
|
1259
|
+
<span class="resource-name">SSE streams</span>
|
|
1260
|
+
<span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
|
|
1261
|
+
</summary>
|
|
1262
|
+
<table><tbody>
|
|
1263
|
+
${rows.join("")}
|
|
1264
|
+
</tbody></table>
|
|
1265
|
+
</details>`;
|
|
1266
|
+
}
|
|
1145
1267
|
function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
|
|
1146
1268
|
const examples = [];
|
|
1147
1269
|
const firstCol = collections[0];
|
|
@@ -1812,6 +1934,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
1812
1934
|
const collections = Object.keys(storage.getData());
|
|
1813
1935
|
const relations = storage.getRelations();
|
|
1814
1936
|
const customRoutes = storage.getRoutes();
|
|
1937
|
+
const sseRoutes = storage.getSseRoutes();
|
|
1815
1938
|
const base = options.base;
|
|
1816
1939
|
const host = `http://${options.host}:${options.port}`;
|
|
1817
1940
|
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
@@ -1995,6 +2118,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
1995
2118
|
${accordions}
|
|
1996
2119
|
${nestedRoutesAccordion(relations, base)}
|
|
1997
2120
|
${options.snapshot ? snapshotAccordion() : ""}
|
|
2121
|
+
${sseRoutesAccordion(sseRoutes, base)}
|
|
1998
2122
|
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
1999
2123
|
${handlersAccordion(handlers, customRoutes, base)}
|
|
2000
2124
|
</div>
|
|
@@ -3125,6 +3249,79 @@ var SnapshotRouteCommand = class {
|
|
|
3125
3249
|
}
|
|
3126
3250
|
};
|
|
3127
3251
|
|
|
3252
|
+
// src/router/routes/sse.routes.ts
|
|
3253
|
+
var SSERouteCommand = class {
|
|
3254
|
+
constructor(storage, base) {
|
|
3255
|
+
this.storage = storage;
|
|
3256
|
+
this.base = base;
|
|
3257
|
+
}
|
|
3258
|
+
storage;
|
|
3259
|
+
base;
|
|
3260
|
+
register(server) {
|
|
3261
|
+
for (const route of this.storage.getSseRoutes()) {
|
|
3262
|
+
const url = `${this.base}${route.path}`;
|
|
3263
|
+
server.route({
|
|
3264
|
+
method: "GET",
|
|
3265
|
+
url,
|
|
3266
|
+
handler: async (req, reply) => {
|
|
3267
|
+
reply.hijack();
|
|
3268
|
+
const raw = reply.raw;
|
|
3269
|
+
raw.setHeader("Content-Type", "text/event-stream");
|
|
3270
|
+
raw.setHeader("Cache-Control", "no-cache");
|
|
3271
|
+
raw.setHeader("Connection", "keep-alive");
|
|
3272
|
+
raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
3273
|
+
raw.flushHeaders();
|
|
3274
|
+
let alive = true;
|
|
3275
|
+
req.raw.on("close", () => {
|
|
3276
|
+
alive = false;
|
|
3277
|
+
});
|
|
3278
|
+
const keepAlive = setInterval(() => {
|
|
3279
|
+
if (alive) raw.write(": ping\n\n");
|
|
3280
|
+
}, 15e3);
|
|
3281
|
+
let frameIndex = 0;
|
|
3282
|
+
let cycleCount = 0;
|
|
3283
|
+
function sendNext() {
|
|
3284
|
+
if (!alive) {
|
|
3285
|
+
clearInterval(keepAlive);
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
const ev = route.events[frameIndex];
|
|
3289
|
+
const ctx = {
|
|
3290
|
+
params: req.params,
|
|
3291
|
+
query: req.query,
|
|
3292
|
+
body: null,
|
|
3293
|
+
headers: req.headers
|
|
3294
|
+
};
|
|
3295
|
+
const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
|
|
3296
|
+
let chunk = "";
|
|
3297
|
+
if (ev.event) chunk += `event: ${ev.event}
|
|
3298
|
+
`;
|
|
3299
|
+
chunk += `data: ${JSON.stringify(data)}
|
|
3300
|
+
|
|
3301
|
+
`;
|
|
3302
|
+
raw.write(chunk);
|
|
3303
|
+
frameIndex++;
|
|
3304
|
+
if (frameIndex >= route.events.length) {
|
|
3305
|
+
frameIndex = 0;
|
|
3306
|
+
cycleCount++;
|
|
3307
|
+
const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
|
|
3308
|
+
if (done) {
|
|
3309
|
+
clearInterval(keepAlive);
|
|
3310
|
+
raw.end();
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
if (alive) {
|
|
3315
|
+
setTimeout(sendNext, route.interval);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
sendNext();
|
|
3319
|
+
}
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
|
|
3128
3325
|
// src/router/resource.router.ts
|
|
3129
3326
|
function buildResourceRouteCommands(storage, options) {
|
|
3130
3327
|
const commands = [];
|
|
@@ -3166,6 +3363,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
|
|
|
3166
3363
|
new AboutRouteCommand(storage, options, handlers),
|
|
3167
3364
|
new OpenApiRouteCommand(storage, options),
|
|
3168
3365
|
...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
|
|
3366
|
+
new SSERouteCommand(storage, options.base),
|
|
3169
3367
|
new CustomRouteCommand(storage, options.base, handlers),
|
|
3170
3368
|
...buildResourceRouteCommands(storage, options)
|
|
3171
3369
|
];
|
|
@@ -3311,11 +3509,18 @@ function registerServe(program2) {
|
|
|
3311
3509
|
"--id-strategy <strategy>",
|
|
3312
3510
|
"Id generation strategy for new items: increment (default) or uuid",
|
|
3313
3511
|
"increment"
|
|
3512
|
+
).option(
|
|
3513
|
+
"--uuid",
|
|
3514
|
+
"Shorthand for --id-strategy uuid: assign UUID ids to new items created via POST"
|
|
3314
3515
|
).action(async (file, flags, cmd) => {
|
|
3315
3516
|
const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
|
|
3316
3517
|
const cliOverrides = Object.fromEntries(
|
|
3317
3518
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
3318
3519
|
);
|
|
3520
|
+
if (cliOverrides["uuid"] === true) {
|
|
3521
|
+
cliOverrides["idStrategy"] = "uuid";
|
|
3522
|
+
delete cliOverrides["uuid"];
|
|
3523
|
+
}
|
|
3319
3524
|
const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
|
|
3320
3525
|
let storage;
|
|
3321
3526
|
try {
|
package/dist/index.d.mts
CHANGED
|
@@ -284,6 +284,42 @@ type CustomRoute = {
|
|
|
284
284
|
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
285
285
|
errorBody?: unknown;
|
|
286
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
|
+
};
|
|
287
323
|
/**
|
|
288
324
|
* In-memory store backed by a YAML file.
|
|
289
325
|
*
|
|
@@ -336,6 +372,11 @@ interface YrestStorage {
|
|
|
336
372
|
* Returns an empty array if no `_routes` block is present.
|
|
337
373
|
*/
|
|
338
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[];
|
|
339
380
|
/**
|
|
340
381
|
* Returns the saved snapshot: a frozen copy of `data` and `relations` taken
|
|
341
382
|
* at the last call to {@link saveSnapshot} (or at construction time).
|
package/dist/index.d.ts
CHANGED
|
@@ -284,6 +284,42 @@ type CustomRoute = {
|
|
|
284
284
|
/** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
|
|
285
285
|
errorBody?: unknown;
|
|
286
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
|
+
};
|
|
287
323
|
/**
|
|
288
324
|
* In-memory store backed by a YAML file.
|
|
289
325
|
*
|
|
@@ -336,6 +372,11 @@ interface YrestStorage {
|
|
|
336
372
|
* Returns an empty array if no `_routes` block is present.
|
|
337
373
|
*/
|
|
338
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[];
|
|
339
380
|
/**
|
|
340
381
|
* Returns the saved snapshot: a frozen copy of `data` and `relations` taken
|
|
341
382
|
* at the last call to {@link saveSnapshot} (or at construction time).
|