@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/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://github.com/aggiovato/yRest/actions)
|
|
11
11
|
[](https://www.npmjs.com/package/@yrest/cli)
|
|
12
12
|
[](https://www.typescriptlang.org/)
|
|
13
|
-
[](https://socket.dev/npm/package/@yrest/cli)
|
|
14
14
|
|
|
15
15
|
YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
|
|
16
16
|
|
|
@@ -75,6 +75,7 @@ A YAML-first alternative to json-server for frontend development.
|
|
|
75
75
|
| OpenAPI 3.0 spec (`GET /_openapi`, `yrest openapi`) | ✅ | ❌ |
|
|
76
76
|
| Field annotations (`_schema`) | ✅ | ❌ |
|
|
77
77
|
| Compact relation DSL (`m2o:target@fk[card]+nested`) | ✅ | ❌ |
|
|
78
|
+
| SSE stream routes (`_method: SSE`) | ✅ | ❌ |
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
@@ -256,6 +257,31 @@ _data:
|
|
|
256
257
|
|
|
257
258
|
Both formats are equivalent. You can also mix them — if the same collection name appears both inside `_data` and at the root level, the root-level entry wins. The file is persisted in whichever format it was originally written.
|
|
258
259
|
|
|
260
|
+
### Data directives (`__uuid_gen`, `__fk`)
|
|
261
|
+
|
|
262
|
+
Use `__uuid_gen` and `__fk` directives to generate UUID-based IDs and cross-collection references automatically. yRest resolves all directives on first load and rewrites the file with real values — subsequent restarts are stable.
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
users:
|
|
266
|
+
- id: __uuid_gen:ana # generates a UUID and registers alias "ana"
|
|
267
|
+
name: Ana
|
|
268
|
+
- id: __uuid_gen:luis # generates a UUID and registers alias "luis"
|
|
269
|
+
name: Luis
|
|
270
|
+
|
|
271
|
+
posts:
|
|
272
|
+
- id: __uuid_gen # generates a UUID (no alias needed)
|
|
273
|
+
title: Hello World
|
|
274
|
+
userId: __fk.users:ana # resolves to the UUID registered as "ana" in users
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
| Directive | Meaning |
|
|
278
|
+
| ----------------------- | ---------------------------------------------------------------- |
|
|
279
|
+
| `__uuid_gen` | Generate a UUID v4 for this field |
|
|
280
|
+
| `__uuid_gen:alias` | Generate a UUID and register it under `alias` in this collection |
|
|
281
|
+
| `__fk.collection:alias` | Resolve to the UUID registered as `alias` in `collection` |
|
|
282
|
+
|
|
283
|
+
Aliases are scoped to their collection — `__uuid_gen:ale` in `users` and `__uuid_gen:ale` in `categories` are independent.
|
|
284
|
+
|
|
259
285
|
---
|
|
260
286
|
|
|
261
287
|
## Field annotations (`_schema`)
|
|
@@ -935,6 +961,76 @@ type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerRespons
|
|
|
935
961
|
|
|
936
962
|
If a named handler is not found in the file, the server returns `501`. If the handler throws, it returns `500`. If a `response:` block is defined alongside `handler:`, it is used as fallback only when the handler itself throws.
|
|
937
963
|
|
|
964
|
+
### SSE streams
|
|
965
|
+
|
|
966
|
+
Mock Server-Sent Events streams by adding `_method: SSE` and an `_sse` block to a `_routes` entry. Clients connect with a regular `GET` request and receive a continuous stream of events until the connection is closed.
|
|
967
|
+
|
|
968
|
+
```yaml
|
|
969
|
+
_routes:
|
|
970
|
+
- _method: SSE
|
|
971
|
+
_path: /events/orders
|
|
972
|
+
_sse:
|
|
973
|
+
_interval: 1500 # ms between frames (default: 1000)
|
|
974
|
+
_loop: true # restart after last event (default: true)
|
|
975
|
+
_repeat: 3 # stop after N full cycles (omit = infinite)
|
|
976
|
+
_events:
|
|
977
|
+
- _event: update
|
|
978
|
+
_data:
|
|
979
|
+
orderId: 1
|
|
980
|
+
status: processing
|
|
981
|
+
- _event: update
|
|
982
|
+
_data:
|
|
983
|
+
orderId: 1
|
|
984
|
+
status: shipped
|
|
985
|
+
- _event: done
|
|
986
|
+
_data:
|
|
987
|
+
orderId: 1
|
|
988
|
+
status: delivered
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
**`_sse` options:**
|
|
992
|
+
|
|
993
|
+
| Key | Default | Description |
|
|
994
|
+
| ----------- | ------- | ---------------------------------------------------------------------------------------------------- |
|
|
995
|
+
| `_interval` | `1000` | Milliseconds between frames |
|
|
996
|
+
| `_loop` | `true` | Restart the sequence after the last frame. Set to `false` to close after one cycle |
|
|
997
|
+
| `_repeat` | — | Stop after N complete cycles and close the stream. Omit for infinite |
|
|
998
|
+
| `_events` | — | Ordered list of frames. Each entry needs `_data` (required) and optionally `_event` (SSE event name) |
|
|
999
|
+
|
|
1000
|
+
**Template variables** — resolved per frame at emit time, so each event gets a fresh value:
|
|
1001
|
+
|
|
1002
|
+
```yaml
|
|
1003
|
+
_events:
|
|
1004
|
+
- _event: tick
|
|
1005
|
+
_data:
|
|
1006
|
+
ts: "{{now}}" # ISO timestamp, unique per frame
|
|
1007
|
+
id: "{{uuid}}" # random UUID v4
|
|
1008
|
+
channel: "{{params.channel}}" # from URL params
|
|
1009
|
+
filter: "{{query.filter}}" # from query string
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
**Consuming from the browser:**
|
|
1013
|
+
|
|
1014
|
+
```js
|
|
1015
|
+
const es = new EventSource("http://localhost:3070/events/orders");
|
|
1016
|
+
|
|
1017
|
+
es.addEventListener("update", (e) => console.log(JSON.parse(e.data)));
|
|
1018
|
+
es.addEventListener("done", (e) => {
|
|
1019
|
+
console.log(JSON.parse(e.data));
|
|
1020
|
+
es.close();
|
|
1021
|
+
});
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
**Consuming from the terminal:**
|
|
1025
|
+
|
|
1026
|
+
```bash
|
|
1027
|
+
curl -N http://localhost:3070/events/orders
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
The `-N` flag disables curl's output buffering, which is required to see SSE frames as they arrive.
|
|
1031
|
+
|
|
1032
|
+
A keep-alive comment (`: ping`) is sent every 15 s to prevent proxy timeouts. SSE routes are shown in `/_about` under their own **SSE streams** accordion with a sky-blue `SSE` badge.
|
|
1033
|
+
|
|
938
1034
|
---
|
|
939
1035
|
|
|
940
1036
|
## Server modes
|
|
@@ -1252,6 +1348,9 @@ const server = createYrestServer({
|
|
|
1252
1348
|
| Explicit relation types (one2one, many2many) | ✅ |
|
|
1253
1349
|
| Bidirectional many2many nested routes | ✅ |
|
|
1254
1350
|
| Auto-embedding (`nested: true` in `_rel`) | ✅ |
|
|
1351
|
+
| SSE stream routes (`_method: SSE`) | ✅ |
|
|
1352
|
+
| WebSocket mock channels (`_method: WS`) | 🔜 |
|
|
1353
|
+
| TypeScript handlers + typed collections | 🔜 |
|
|
1255
1354
|
|
|
1256
1355
|
---
|
|
1257
1356
|
|
package/dist/cli/index.js
CHANGED
|
@@ -115,6 +115,7 @@ categories:
|
|
|
115
115
|
// src/cli/commands/templates/relational.ts
|
|
116
116
|
var relationalTemplate = `# yrest relational sample \u2014 blog
|
|
117
117
|
# Demonstrates: many2one, one2one and many2many relationships
|
|
118
|
+
# Also shows __uuid_gen and __fk directives for UUID-based IDs
|
|
118
119
|
# Run: npx @yrest/cli serve db.yml
|
|
119
120
|
# Docs: GET http://localhost:3070/_about
|
|
120
121
|
|
|
@@ -139,33 +140,36 @@ _rel:
|
|
|
139
140
|
_type: one2one
|
|
140
141
|
_target: users
|
|
141
142
|
|
|
143
|
+
# __uuid_gen:alias generates a UUID and registers it under that alias.
|
|
144
|
+
# __fk.collection:alias resolves to the UUID registered in that collection.
|
|
145
|
+
# yRest rewrites this file on first run, replacing all directives with real values.
|
|
142
146
|
users:
|
|
143
|
-
- id:
|
|
147
|
+
- id: __uuid_gen:ana
|
|
144
148
|
name: Ana Garc\xEDa
|
|
145
149
|
email: ana@example.com
|
|
146
150
|
role: author
|
|
147
|
-
- id:
|
|
151
|
+
- id: __uuid_gen:luis
|
|
148
152
|
name: Luis Mart\xEDnez
|
|
149
153
|
email: luis@example.com
|
|
150
154
|
role: author
|
|
151
|
-
- id:
|
|
155
|
+
- id: __uuid_gen:sara
|
|
152
156
|
name: Sara L\xF3pez
|
|
153
157
|
email: sara@example.com
|
|
154
158
|
role: reader
|
|
155
159
|
|
|
156
160
|
profiles:
|
|
157
|
-
- id:
|
|
158
|
-
userId:
|
|
161
|
+
- id: __uuid_gen
|
|
162
|
+
userId: __fk.users:ana
|
|
159
163
|
bio: Full-stack developer and open-source enthusiast
|
|
160
164
|
avatar: https://i.pravatar.cc/150?img=1
|
|
161
165
|
website: https://ana.dev
|
|
162
|
-
- id:
|
|
163
|
-
userId:
|
|
166
|
+
- id: __uuid_gen
|
|
167
|
+
userId: __fk.users:luis
|
|
164
168
|
bio: Backend engineer, coffee addict
|
|
165
169
|
avatar: https://i.pravatar.cc/150?img=2
|
|
166
170
|
website: https://luisdev.io
|
|
167
|
-
- id:
|
|
168
|
-
userId:
|
|
171
|
+
- id: __uuid_gen
|
|
172
|
+
userId: __fk.users:sara
|
|
169
173
|
bio: Designer turned frontend developer
|
|
170
174
|
avatar: https://i.pravatar.cc/150?img=3
|
|
171
175
|
website: null
|
|
@@ -188,75 +192,75 @@ tags:
|
|
|
188
192
|
color: "#ef4444"
|
|
189
193
|
|
|
190
194
|
posts:
|
|
191
|
-
- id:
|
|
195
|
+
- id: __uuid_gen:post-ts
|
|
192
196
|
title: Getting started with TypeScript
|
|
193
197
|
slug: getting-started-typescript
|
|
194
198
|
body: TypeScript adds static typing to JavaScript, catching errors at compile time...
|
|
195
|
-
userId:
|
|
199
|
+
userId: __fk.users:ana
|
|
196
200
|
published: true
|
|
197
201
|
views: 1420
|
|
198
202
|
createdAt: "2024-11-01"
|
|
199
|
-
- id:
|
|
203
|
+
- id: __uuid_gen:post-fastify
|
|
200
204
|
title: Building REST APIs with Fastify
|
|
201
205
|
slug: rest-apis-fastify
|
|
202
206
|
body: Fastify is the fastest Node.js web framework, perfect for building APIs...
|
|
203
|
-
userId:
|
|
207
|
+
userId: __fk.users:ana
|
|
204
208
|
published: true
|
|
205
209
|
views: 980
|
|
206
210
|
createdAt: "2024-11-15"
|
|
207
|
-
- id:
|
|
211
|
+
- id: __uuid_gen:post-testing
|
|
208
212
|
title: Testing strategies for modern apps
|
|
209
213
|
slug: testing-strategies-modern
|
|
210
214
|
body: A solid test strategy covers unit, integration and end-to-end scenarios...
|
|
211
|
-
userId:
|
|
215
|
+
userId: __fk.users:luis
|
|
212
216
|
published: true
|
|
213
217
|
views: 640
|
|
214
218
|
createdAt: "2024-12-03"
|
|
215
|
-
- id:
|
|
219
|
+
- id: __uuid_gen:post-yaml
|
|
216
220
|
title: YAML as a database format
|
|
217
221
|
slug: yaml-database-format
|
|
218
222
|
body: YAML is human-readable and expressive enough for mock data during development...
|
|
219
|
-
userId:
|
|
223
|
+
userId: __fk.users:luis
|
|
220
224
|
published: false
|
|
221
225
|
views: 0
|
|
222
226
|
createdAt: "2025-01-10"
|
|
223
227
|
|
|
224
228
|
# pivot table for posts \u2194 tags (many2many)
|
|
225
229
|
post_tags:
|
|
226
|
-
- { id: 1, postId:
|
|
227
|
-
- { id: 2, postId:
|
|
228
|
-
- { id: 3, postId:
|
|
229
|
-
- { id: 4, postId:
|
|
230
|
-
- { id: 5, postId:
|
|
231
|
-
- { id: 6, postId:
|
|
232
|
-
- { id: 7, postId:
|
|
233
|
-
- { id: 8, postId:
|
|
230
|
+
- { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
|
|
231
|
+
- { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
|
|
232
|
+
- { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
|
|
233
|
+
- { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
|
|
234
|
+
- { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
|
|
235
|
+
- { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
|
|
236
|
+
- { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
|
|
237
|
+
- { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
|
|
234
238
|
|
|
235
239
|
comments:
|
|
236
240
|
- id: 1
|
|
237
241
|
body: Great introduction! This helped me a lot.
|
|
238
|
-
postId:
|
|
239
|
-
userId:
|
|
242
|
+
postId: __fk.posts:post-ts
|
|
243
|
+
userId: __fk.users:sara
|
|
240
244
|
likes: 5
|
|
241
245
|
- id: 2
|
|
242
246
|
body: Could you cover generics in a follow-up post?
|
|
243
|
-
postId:
|
|
244
|
-
userId:
|
|
247
|
+
postId: __fk.posts:post-ts
|
|
248
|
+
userId: __fk.users:luis
|
|
245
249
|
likes: 3
|
|
246
250
|
- id: 3
|
|
247
251
|
body: Fastify is indeed much faster than Express in my benchmarks.
|
|
248
|
-
postId:
|
|
249
|
-
userId:
|
|
252
|
+
postId: __fk.posts:post-fastify
|
|
253
|
+
userId: __fk.users:sara
|
|
250
254
|
likes: 8
|
|
251
255
|
- id: 4
|
|
252
256
|
body: Do you have a GitHub repo with these examples?
|
|
253
|
-
postId:
|
|
254
|
-
userId:
|
|
257
|
+
postId: __fk.posts:post-fastify
|
|
258
|
+
userId: __fk.users:ana
|
|
255
259
|
likes: 1
|
|
256
260
|
- id: 5
|
|
257
261
|
body: E2E tests are underrated. Solid post!
|
|
258
|
-
postId:
|
|
259
|
-
userId:
|
|
262
|
+
postId: __fk.posts:post-testing
|
|
263
|
+
userId: __fk.users:ana
|
|
260
264
|
likes: 4
|
|
261
265
|
|
|
262
266
|
# Try these queries:
|
|
@@ -546,7 +550,7 @@ var import_node_path4 = require("path");
|
|
|
546
550
|
// src/storage/yrestStorage.ts
|
|
547
551
|
var import_node_fs2 = require("fs");
|
|
548
552
|
var import_node_path2 = require("path");
|
|
549
|
-
var
|
|
553
|
+
var import_node_crypto2 = require("crypto");
|
|
550
554
|
var import_yaml = require("yaml");
|
|
551
555
|
|
|
552
556
|
// src/storage/parseRelations.ts
|
|
@@ -664,6 +668,8 @@ function normaliseRoute(raw) {
|
|
|
664
668
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
665
669
|
const v = raw;
|
|
666
670
|
const method = v["_method"];
|
|
671
|
+
if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
|
|
672
|
+
if ("_sse" in v) return null;
|
|
667
673
|
const path = v["_path"];
|
|
668
674
|
if (typeof method !== "string" || typeof path !== "string") return null;
|
|
669
675
|
const route = { method, path };
|
|
@@ -703,6 +709,43 @@ function normaliseScenario(raw) {
|
|
|
703
709
|
return { when, response };
|
|
704
710
|
}
|
|
705
711
|
|
|
712
|
+
// src/storage/parseSseRoutes.ts
|
|
713
|
+
function parseSseRoutes(raw) {
|
|
714
|
+
if (!Array.isArray(raw)) return [];
|
|
715
|
+
return raw.map(normaliseSseRoute).filter((r) => r !== null);
|
|
716
|
+
}
|
|
717
|
+
function normaliseSseRoute(raw) {
|
|
718
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
719
|
+
const v = raw;
|
|
720
|
+
const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
|
|
721
|
+
const path = v["_path"];
|
|
722
|
+
const sse = v["_sse"];
|
|
723
|
+
if (method !== "SSE") return null;
|
|
724
|
+
if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
const s = sse;
|
|
728
|
+
const rawEvents = s["_events"];
|
|
729
|
+
if (!Array.isArray(rawEvents)) return null;
|
|
730
|
+
const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
|
|
731
|
+
if (!events.length) return null;
|
|
732
|
+
return {
|
|
733
|
+
path,
|
|
734
|
+
interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
|
|
735
|
+
loop: s["_loop"] !== false,
|
|
736
|
+
repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
|
|
737
|
+
events
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function normaliseEvent(raw) {
|
|
741
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
742
|
+
const v = raw;
|
|
743
|
+
if (!("_data" in v)) return null;
|
|
744
|
+
const event = { data: v["_data"] };
|
|
745
|
+
if (typeof v["_event"] === "string") event.event = v["_event"];
|
|
746
|
+
return event;
|
|
747
|
+
}
|
|
748
|
+
|
|
706
749
|
// src/storage/parseSchema.ts
|
|
707
750
|
function parseSchema(raw) {
|
|
708
751
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
@@ -779,6 +822,51 @@ function hasDataBlock(raw) {
|
|
|
779
822
|
return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
|
|
780
823
|
}
|
|
781
824
|
|
|
825
|
+
// src/storage/resolveDirectives.ts
|
|
826
|
+
var import_node_crypto = require("crypto");
|
|
827
|
+
var UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
|
|
828
|
+
var FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
|
|
829
|
+
function resolveDirectives(data) {
|
|
830
|
+
const registry = /* @__PURE__ */ new Map();
|
|
831
|
+
let modified = false;
|
|
832
|
+
for (const [collection, items] of Object.entries(data)) {
|
|
833
|
+
for (const item of items) {
|
|
834
|
+
for (const [field, value] of Object.entries(item)) {
|
|
835
|
+
if (typeof value !== "string") continue;
|
|
836
|
+
const m = UUID_GEN_RE.exec(value);
|
|
837
|
+
if (!m) continue;
|
|
838
|
+
const uuid = (0, import_node_crypto.randomUUID)();
|
|
839
|
+
item[field] = uuid;
|
|
840
|
+
modified = true;
|
|
841
|
+
const alias = m[1];
|
|
842
|
+
if (alias) {
|
|
843
|
+
if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
|
|
844
|
+
registry.get(collection).set(alias, uuid);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (const items of Object.values(data)) {
|
|
850
|
+
for (const item of items) {
|
|
851
|
+
for (const [field, value] of Object.entries(item)) {
|
|
852
|
+
if (typeof value !== "string") continue;
|
|
853
|
+
const m = FK_RE.exec(value);
|
|
854
|
+
if (!m) continue;
|
|
855
|
+
const col = m[1];
|
|
856
|
+
const alias = m[2];
|
|
857
|
+
const uuid = registry.get(col)?.get(alias);
|
|
858
|
+
if (uuid !== void 0) {
|
|
859
|
+
item[field] = uuid;
|
|
860
|
+
modified = true;
|
|
861
|
+
} else {
|
|
862
|
+
console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return modified;
|
|
868
|
+
}
|
|
869
|
+
|
|
782
870
|
// src/utils/deepCopy.ts
|
|
783
871
|
function deepCopyData(source) {
|
|
784
872
|
return Object.fromEntries(
|
|
@@ -846,15 +934,17 @@ function createYrestStorage(filePath) {
|
|
|
846
934
|
const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
|
|
847
935
|
const relations = parseRelations(raw["_rel"]);
|
|
848
936
|
const routes = parseRoutes(raw["_routes"]);
|
|
937
|
+
const sseRoutes = parseSseRoutes(raw["_routes"]);
|
|
849
938
|
const schema = parseSchema(raw["_schema"]);
|
|
850
939
|
const dataBlock = hasDataBlock(raw);
|
|
851
940
|
const data = parseData(raw);
|
|
941
|
+
const directivesModified = resolveDirectives(data);
|
|
852
942
|
let snapshot = {
|
|
853
943
|
data: deepCopyData(data),
|
|
854
944
|
relations: { ...relations },
|
|
855
945
|
savedAt: /* @__PURE__ */ new Date()
|
|
856
946
|
};
|
|
857
|
-
|
|
947
|
+
const storage = {
|
|
858
948
|
getData() {
|
|
859
949
|
return data;
|
|
860
950
|
},
|
|
@@ -867,6 +957,9 @@ function createYrestStorage(filePath) {
|
|
|
867
957
|
getRoutes() {
|
|
868
958
|
return routes;
|
|
869
959
|
},
|
|
960
|
+
getSseRoutes() {
|
|
961
|
+
return sseRoutes;
|
|
962
|
+
},
|
|
870
963
|
getCollection(name) {
|
|
871
964
|
return data[name];
|
|
872
965
|
},
|
|
@@ -882,7 +975,7 @@ function createYrestStorage(filePath) {
|
|
|
882
975
|
} else {
|
|
883
976
|
Object.assign(payload, data);
|
|
884
977
|
}
|
|
885
|
-
const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0,
|
|
978
|
+
const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
|
|
886
979
|
(0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml.stringify)(payload), "utf8");
|
|
887
980
|
(0, import_node_fs2.renameSync)(tmp, absPath);
|
|
888
981
|
},
|
|
@@ -894,6 +987,7 @@ function createYrestStorage(filePath) {
|
|
|
894
987
|
Object.assign(data, freshData);
|
|
895
988
|
for (const key of Object.keys(relations)) delete relations[key];
|
|
896
989
|
Object.assign(relations, freshRelations);
|
|
990
|
+
if (resolveDirectives(data)) storage.persist();
|
|
897
991
|
},
|
|
898
992
|
getSnapshot() {
|
|
899
993
|
return snapshot;
|
|
@@ -914,6 +1008,8 @@ function createYrestStorage(filePath) {
|
|
|
914
1008
|
this.persist();
|
|
915
1009
|
}
|
|
916
1010
|
};
|
|
1011
|
+
if (directivesModified) storage.persist();
|
|
1012
|
+
return storage;
|
|
917
1013
|
}
|
|
918
1014
|
|
|
919
1015
|
// src/server/createServer.ts
|
|
@@ -926,7 +1022,7 @@ var import_node_path3 = require("path");
|
|
|
926
1022
|
var import_node_url = require("url");
|
|
927
1023
|
|
|
928
1024
|
// src/utils/interpolate.ts
|
|
929
|
-
var
|
|
1025
|
+
var import_node_crypto3 = require("crypto");
|
|
930
1026
|
function getPath(obj, path) {
|
|
931
1027
|
return path.split(".").reduce((acc, key) => {
|
|
932
1028
|
if (acc != null && typeof acc === "object") return acc[key];
|
|
@@ -935,7 +1031,7 @@ function getPath(obj, path) {
|
|
|
935
1031
|
}
|
|
936
1032
|
function resolveVar(path, ctx) {
|
|
937
1033
|
if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
|
|
938
|
-
if (path === "uuid") return (0,
|
|
1034
|
+
if (path === "uuid") return (0, import_node_crypto3.randomUUID)();
|
|
939
1035
|
if (path === "body") return ctx.body;
|
|
940
1036
|
if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
|
|
941
1037
|
if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
|
|
@@ -978,6 +1074,8 @@ var METHOD_COLOR = {
|
|
|
978
1074
|
PUT: "#d29922",
|
|
979
1075
|
PATCH: "#a371f7",
|
|
980
1076
|
DELETE: "#f85149",
|
|
1077
|
+
SSE: "#0ea5e9",
|
|
1078
|
+
WS: "#8b5cf6",
|
|
981
1079
|
fn: "#f0883e"
|
|
982
1080
|
};
|
|
983
1081
|
function escapeHtml(str) {
|
|
@@ -1169,6 +1267,30 @@ function handlersAccordion(handlers, routes, base) {
|
|
|
1169
1267
|
</tbody></table>
|
|
1170
1268
|
</details>`;
|
|
1171
1269
|
}
|
|
1270
|
+
function sseRoutesAccordion(routes, base) {
|
|
1271
|
+
if (!routes.length) return "";
|
|
1272
|
+
const rows = routes.map((r) => {
|
|
1273
|
+
const tags = [
|
|
1274
|
+
`<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
|
|
1275
|
+
`<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
|
|
1276
|
+
];
|
|
1277
|
+
if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
|
|
1278
|
+
if (r.repeat !== void 0)
|
|
1279
|
+
tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
|
|
1280
|
+
const desc = `SSE stream ${tags.join(" ")}`;
|
|
1281
|
+
return endpointRow("SSE", `${base}${r.path}`, desc);
|
|
1282
|
+
});
|
|
1283
|
+
return `
|
|
1284
|
+
<details class="resource-card nested-card">
|
|
1285
|
+
<summary>
|
|
1286
|
+
<span class="resource-name">SSE streams</span>
|
|
1287
|
+
<span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
|
|
1288
|
+
</summary>
|
|
1289
|
+
<table><tbody>
|
|
1290
|
+
${rows.join("")}
|
|
1291
|
+
</tbody></table>
|
|
1292
|
+
</details>`;
|
|
1293
|
+
}
|
|
1172
1294
|
function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
|
|
1173
1295
|
const examples = [];
|
|
1174
1296
|
const firstCol = collections[0];
|
|
@@ -1839,6 +1961,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
1839
1961
|
const collections = Object.keys(storage.getData());
|
|
1840
1962
|
const relations = storage.getRelations();
|
|
1841
1963
|
const customRoutes = storage.getRoutes();
|
|
1964
|
+
const sseRoutes = storage.getSseRoutes();
|
|
1842
1965
|
const base = options.base;
|
|
1843
1966
|
const host = `http://${options.host}:${options.port}`;
|
|
1844
1967
|
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
@@ -2022,6 +2145,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
2022
2145
|
${accordions}
|
|
2023
2146
|
${nestedRoutesAccordion(relations, base)}
|
|
2024
2147
|
${options.snapshot ? snapshotAccordion() : ""}
|
|
2148
|
+
${sseRoutesAccordion(sseRoutes, base)}
|
|
2025
2149
|
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
2026
2150
|
${handlersAccordion(handlers, customRoutes, base)}
|
|
2027
2151
|
</div>
|
|
@@ -3152,6 +3276,79 @@ var SnapshotRouteCommand = class {
|
|
|
3152
3276
|
}
|
|
3153
3277
|
};
|
|
3154
3278
|
|
|
3279
|
+
// src/router/routes/sse.routes.ts
|
|
3280
|
+
var SSERouteCommand = class {
|
|
3281
|
+
constructor(storage, base) {
|
|
3282
|
+
this.storage = storage;
|
|
3283
|
+
this.base = base;
|
|
3284
|
+
}
|
|
3285
|
+
storage;
|
|
3286
|
+
base;
|
|
3287
|
+
register(server) {
|
|
3288
|
+
for (const route of this.storage.getSseRoutes()) {
|
|
3289
|
+
const url = `${this.base}${route.path}`;
|
|
3290
|
+
server.route({
|
|
3291
|
+
method: "GET",
|
|
3292
|
+
url,
|
|
3293
|
+
handler: async (req, reply) => {
|
|
3294
|
+
reply.hijack();
|
|
3295
|
+
const raw = reply.raw;
|
|
3296
|
+
raw.setHeader("Content-Type", "text/event-stream");
|
|
3297
|
+
raw.setHeader("Cache-Control", "no-cache");
|
|
3298
|
+
raw.setHeader("Connection", "keep-alive");
|
|
3299
|
+
raw.setHeader("Access-Control-Allow-Origin", "*");
|
|
3300
|
+
raw.flushHeaders();
|
|
3301
|
+
let alive = true;
|
|
3302
|
+
req.raw.on("close", () => {
|
|
3303
|
+
alive = false;
|
|
3304
|
+
});
|
|
3305
|
+
const keepAlive = setInterval(() => {
|
|
3306
|
+
if (alive) raw.write(": ping\n\n");
|
|
3307
|
+
}, 15e3);
|
|
3308
|
+
let frameIndex = 0;
|
|
3309
|
+
let cycleCount = 0;
|
|
3310
|
+
function sendNext() {
|
|
3311
|
+
if (!alive) {
|
|
3312
|
+
clearInterval(keepAlive);
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
const ev = route.events[frameIndex];
|
|
3316
|
+
const ctx = {
|
|
3317
|
+
params: req.params,
|
|
3318
|
+
query: req.query,
|
|
3319
|
+
body: null,
|
|
3320
|
+
headers: req.headers
|
|
3321
|
+
};
|
|
3322
|
+
const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
|
|
3323
|
+
let chunk = "";
|
|
3324
|
+
if (ev.event) chunk += `event: ${ev.event}
|
|
3325
|
+
`;
|
|
3326
|
+
chunk += `data: ${JSON.stringify(data)}
|
|
3327
|
+
|
|
3328
|
+
`;
|
|
3329
|
+
raw.write(chunk);
|
|
3330
|
+
frameIndex++;
|
|
3331
|
+
if (frameIndex >= route.events.length) {
|
|
3332
|
+
frameIndex = 0;
|
|
3333
|
+
cycleCount++;
|
|
3334
|
+
const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
|
|
3335
|
+
if (done) {
|
|
3336
|
+
clearInterval(keepAlive);
|
|
3337
|
+
raw.end();
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (alive) {
|
|
3342
|
+
setTimeout(sendNext, route.interval);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
sendNext();
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
|
|
3155
3352
|
// src/router/resource.router.ts
|
|
3156
3353
|
function buildResourceRouteCommands(storage, options) {
|
|
3157
3354
|
const commands = [];
|
|
@@ -3193,6 +3390,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
|
|
|
3193
3390
|
new AboutRouteCommand(storage, options, handlers),
|
|
3194
3391
|
new OpenApiRouteCommand(storage, options),
|
|
3195
3392
|
...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
|
|
3393
|
+
new SSERouteCommand(storage, options.base),
|
|
3196
3394
|
new CustomRouteCommand(storage, options.base, handlers),
|
|
3197
3395
|
...buildResourceRouteCommands(storage, options)
|
|
3198
3396
|
];
|
|
@@ -3338,11 +3536,18 @@ function registerServe(program2) {
|
|
|
3338
3536
|
"--id-strategy <strategy>",
|
|
3339
3537
|
"Id generation strategy for new items: increment (default) or uuid",
|
|
3340
3538
|
"increment"
|
|
3539
|
+
).option(
|
|
3540
|
+
"--uuid",
|
|
3541
|
+
"Shorthand for --id-strategy uuid: assign UUID ids to new items created via POST"
|
|
3341
3542
|
).action(async (file, flags, cmd) => {
|
|
3342
3543
|
const fileConfig = loadConfigFile((0, import_node_path4.join)(process.cwd(), "yrest.config.yml"));
|
|
3343
3544
|
const cliOverrides = Object.fromEntries(
|
|
3344
3545
|
Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
|
|
3345
3546
|
);
|
|
3547
|
+
if (cliOverrides["uuid"] === true) {
|
|
3548
|
+
cliOverrides["idStrategy"] = "uuid";
|
|
3549
|
+
delete cliOverrides["uuid"];
|
|
3550
|
+
}
|
|
3346
3551
|
const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
|
|
3347
3552
|
let storage;
|
|
3348
3553
|
try {
|