@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.
@@ -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" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
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 data = Object.fromEntries(
779
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
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
- Object.assign(payload, data);
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 = Object.fromEntries(
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&ensp;${tags.join("&ensp;")}`;
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 merged = {
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 and (Phase B) validated on POST/PUT. */
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 and (Phase B) validated on POST/PUT. */
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).