@aggiovato/yrest 0.2.2 → 0.4.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/dist/index.mjs CHANGED
@@ -3,6 +3,11 @@ import { readFileSync, writeFileSync, renameSync } from "fs";
3
3
  import { resolve, dirname } from "path";
4
4
  import { randomUUID } from "crypto";
5
5
  import { parse, stringify } from "yaml";
6
+ function deepCopyData(source) {
7
+ return Object.fromEntries(
8
+ Object.entries(source).map(([k, v]) => [k, v.map((item) => ({ ...item }))])
9
+ );
10
+ }
6
11
  function createYamlStorage(filePath) {
7
12
  const absPath = resolve(filePath);
8
13
  const raw = parse(readFileSync(absPath, "utf8")) ?? {};
@@ -10,6 +15,11 @@ function createYamlStorage(filePath) {
10
15
  const data = Object.fromEntries(
11
16
  Object.entries(raw).filter(([key]) => key !== "_rel")
12
17
  );
18
+ let snapshot = {
19
+ data: deepCopyData(data),
20
+ relations: { ...relations },
21
+ savedAt: /* @__PURE__ */ new Date()
22
+ };
13
23
  return {
14
24
  getData() {
15
25
  return data;
@@ -39,6 +49,24 @@ function createYamlStorage(filePath) {
39
49
  Object.assign(data, freshData);
40
50
  for (const key of Object.keys(relations)) delete relations[key];
41
51
  Object.assign(relations, freshRelations);
52
+ },
53
+ getSnapshot() {
54
+ return snapshot;
55
+ },
56
+ saveSnapshot() {
57
+ snapshot = {
58
+ data: deepCopyData(data),
59
+ relations: { ...relations },
60
+ savedAt: /* @__PURE__ */ new Date()
61
+ };
62
+ },
63
+ resetToSnapshot() {
64
+ const snap = deepCopyData(snapshot.data);
65
+ for (const key of Object.keys(data)) delete data[key];
66
+ Object.assign(data, snap);
67
+ for (const key of Object.keys(relations)) delete relations[key];
68
+ Object.assign(relations, { ...snapshot.relations });
69
+ this.persist();
42
70
  }
43
71
  };
44
72
  }
@@ -47,11 +75,92 @@ function createYamlStorage(filePath) {
47
75
  import Fastify from "fastify";
48
76
  import cors from "@fastify/cors";
49
77
 
50
- // src/services/resourceService.ts
78
+ // src/utils/params.ts
51
79
  function nextId(items) {
52
80
  const ids = items.map((i) => i["id"]).filter((id) => typeof id === "number");
53
81
  return ids.length > 0 ? Math.max(...ids) + 1 : 1;
54
82
  }
83
+ function firstParam(value) {
84
+ if (value === void 0) return void 0;
85
+ return Array.isArray(value) ? value[0] : value;
86
+ }
87
+
88
+ // src/services/query.service.ts
89
+ var OPERATORS = ["_gte", "_lte", "_ne", "_like", "_start", "_regex"];
90
+ function applyOperator(itemValue, op, filterValue) {
91
+ const strItem = String(itemValue);
92
+ const numItem = Number(itemValue);
93
+ const numFilter = Number(filterValue);
94
+ const numeric = !isNaN(numItem) && !isNaN(numFilter) && filterValue.trim() !== "";
95
+ switch (op) {
96
+ case "_gte":
97
+ return numeric ? numItem >= numFilter : strItem >= filterValue;
98
+ case "_lte":
99
+ return numeric ? numItem <= numFilter : strItem <= filterValue;
100
+ case "_ne":
101
+ return strItem !== filterValue;
102
+ case "_like":
103
+ return strItem.toLowerCase().includes(filterValue.toLowerCase());
104
+ case "_start":
105
+ return strItem.toLowerCase().startsWith(filterValue.toLowerCase());
106
+ case "_regex": {
107
+ try {
108
+ return new RegExp(filterValue, "i").test(strItem);
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ function filterByQuery(items, query) {
116
+ const filters = Object.entries(query).filter(([key]) => !key.startsWith("_"));
117
+ if (filters.length === 0) return items;
118
+ return items.filter(
119
+ (item) => filters.every(([key, value]) => {
120
+ const op = OPERATORS.find((o) => key.endsWith(o));
121
+ if (op) {
122
+ const field = key.slice(0, -op.length);
123
+ if (item[field] === void 0) return false;
124
+ const filterVal = Array.isArray(value) ? value[0] : value;
125
+ return applyOperator(item[field], op, filterVal);
126
+ }
127
+ if (item[key] === void 0) return false;
128
+ const itemStr = String(item[key]);
129
+ return Array.isArray(value) ? value.includes(itemStr) : itemStr === value;
130
+ })
131
+ );
132
+ }
133
+ function fullTextSearch(items, term) {
134
+ const lower = term.toLowerCase();
135
+ return items.filter(
136
+ (item) => Object.values(item).some((val) => {
137
+ if (val === null || val === void 0 || typeof val === "object") return false;
138
+ return String(val).toLowerCase().includes(lower);
139
+ })
140
+ );
141
+ }
142
+ function sortBy(items, field, order) {
143
+ const direction = order === "desc" ? -1 : 1;
144
+ return [...items].sort((a, b) => {
145
+ const av = a[field];
146
+ const bv = b[field];
147
+ if (av === void 0) return 1;
148
+ if (bv === void 0) return -1;
149
+ if (typeof av === "number" && typeof bv === "number") return (av - bv) * direction;
150
+ return String(av).localeCompare(String(bv), void 0, { sensitivity: "base" }) * direction;
151
+ });
152
+ }
153
+ function projectFields(input, fields) {
154
+ if (fields.length === 0) return input;
155
+ const project = (item) => Object.fromEntries(fields.filter((f) => f in item).map((f) => [f, item[f]]));
156
+ return Array.isArray(input) ? input.map(project) : project(input);
157
+ }
158
+ function paginate(items, page, limit) {
159
+ const start = (page - 1) * limit;
160
+ return items.slice(start, start + limit);
161
+ }
162
+
163
+ // src/services/resource.service.ts
55
164
  function findById(items, id) {
56
165
  return items.find((i) => String(i["id"]) === id);
57
166
  }
@@ -88,36 +197,17 @@ function patchItem(storage, resource, id, body) {
88
197
  storage.persist();
89
198
  return updated;
90
199
  }
91
- function firstParam(value) {
92
- if (value === void 0) return void 0;
93
- return Array.isArray(value) ? value[0] : value;
94
- }
95
- function filterByQuery(items, query) {
96
- const filters = Object.entries(query).filter(([key]) => !key.startsWith("_"));
97
- if (filters.length === 0) return items;
98
- return items.filter(
99
- (item) => filters.every(([key, value]) => {
100
- if (item[key] === void 0) return false;
101
- const itemStr = String(item[key]);
102
- return Array.isArray(value) ? value.includes(itemStr) : itemStr === value;
103
- })
104
- );
105
- }
106
- function sortBy(items, field, order) {
107
- const direction = order === "desc" ? -1 : 1;
108
- return [...items].sort((a, b) => {
109
- const av = a[field];
110
- const bv = b[field];
111
- if (av === void 0) return 1;
112
- if (bv === void 0) return -1;
113
- if (typeof av === "number" && typeof bv === "number") return (av - bv) * direction;
114
- return String(av).localeCompare(String(bv), void 0, { sensitivity: "base" }) * direction;
115
- });
116
- }
117
- function paginate(items, page, limit) {
118
- const start = (page - 1) * limit;
119
- return items.slice(start, start + limit);
200
+ function deleteItem(storage, resource, id) {
201
+ const collection = storage.getCollection(resource) ?? [];
202
+ const idx = findIndexById(collection, id);
203
+ if (idx === -1) return void 0;
204
+ const [deleted] = collection.splice(idx, 1);
205
+ storage.setCollection(resource, collection);
206
+ storage.persist();
207
+ return deleted;
120
208
  }
209
+
210
+ // src/services/expand.service.ts
121
211
  function expandItems(input, query, resource, storage) {
122
212
  const isArray = Array.isArray(input);
123
213
  const items = isArray ? input : [input];
@@ -150,14 +240,35 @@ function expandItems(input, query, resource, storage) {
150
240
  });
151
241
  return isArray ? expanded : expanded[0];
152
242
  }
153
- function deleteItem(storage, resource, id) {
154
- const collection = storage.getCollection(resource) ?? [];
155
- const idx = findIndexById(collection, id);
156
- if (idx === -1) return void 0;
157
- const [deleted] = collection.splice(idx, 1);
158
- storage.setCollection(resource, collection);
159
- storage.persist();
160
- return deleted;
243
+ function embedItems(input, query, resource, storage) {
244
+ const isArray = Array.isArray(input);
245
+ const items = isArray ? input : [input];
246
+ const embedParam = query["_embed"];
247
+ if (!embedParam) return isArray ? items : input;
248
+ const keys = (Array.isArray(embedParam) ? embedParam : [embedParam]).flatMap((v) => v.split(",")).map((k) => k.trim()).filter(Boolean);
249
+ const relations = storage.getRelations();
250
+ const embeds = /* @__PURE__ */ new Map();
251
+ for (const embedKey of keys) {
252
+ outer: for (const [childCollection, fields] of Object.entries(relations)) {
253
+ for (const [fkField, parentCollection] of Object.entries(fields)) {
254
+ if (parentCollection === resource && childCollection === embedKey) {
255
+ embeds.set(embedKey, { childCollection, fkField });
256
+ break outer;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ if (embeds.size === 0) return isArray ? items : input;
262
+ const result = items.map((item) => {
263
+ const out = { ...item };
264
+ for (const [embedKey, { childCollection, fkField }] of embeds) {
265
+ out[embedKey] = (storage.getCollection(childCollection) ?? []).filter(
266
+ (child) => String(child[fkField]) === String(item["id"])
267
+ );
268
+ }
269
+ return out;
270
+ });
271
+ return isArray ? result : result[0];
161
272
  }
162
273
 
163
274
  // src/router/routes/collection.routes.ts
@@ -165,9 +276,12 @@ var registerCollectionRoutes = (server, storage, resource, base, options) => {
165
276
  server.get(base, (req, reply) => {
166
277
  const collection = storage.getCollection(resource) ?? [];
167
278
  const filtered = filterByQuery(collection, req.query);
279
+ const searchTerm = firstParam(req.query["_q"]);
280
+ const searched = searchTerm ? fullTextSearch(filtered, searchTerm) : filtered;
281
+ const fields = (firstParam(req.query["_fields"]) ?? "").split(",").map((f) => f.trim()).filter(Boolean);
168
282
  const sortField = firstParam(req.query["_sort"]);
169
283
  const sortOrder = firstParam(req.query["_order"]) === "desc" ? "desc" : "asc";
170
- const sorted = sortField ? sortBy(filtered, sortField, sortOrder) : filtered;
284
+ const sorted = sortField ? sortBy(searched, sortField, sortOrder) : searched;
171
285
  if (options.pageable.enabled) {
172
286
  const defaultLimit = options.pageable.limit;
173
287
  const page = Math.max(1, parseInt(firstParam(req.query["_page"]) ?? "1", 10) || 1);
@@ -177,7 +291,15 @@ var registerCollectionRoutes = (server, storage, resource, base, options) => {
177
291
  );
178
292
  const totalItems = sorted.length;
179
293
  const totalPages = Math.ceil(totalItems / limit) || 1;
180
- const data = expandItems(paginate(sorted, page, limit), req.query, resource, storage);
294
+ const data = projectFields(
295
+ embedItems(
296
+ expandItems(paginate(sorted, page, limit), req.query, resource, storage),
297
+ req.query,
298
+ resource,
299
+ storage
300
+ ),
301
+ fields
302
+ );
181
303
  const pagination = {
182
304
  page,
183
305
  limit,
@@ -201,9 +323,15 @@ var registerCollectionRoutes = (server, storage, resource, base, options) => {
201
323
  reply.header("X-Total-Count", String(sorted.length));
202
324
  result = paginate(sorted, page, limit);
203
325
  }
204
- return expandItems(result, req.query, resource, storage);
326
+ return projectFields(
327
+ embedItems(expandItems(result, req.query, resource, storage), req.query, resource, storage),
328
+ fields
329
+ );
205
330
  });
206
331
  server.post(base, (req, reply) => {
332
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
333
+ return reply.status(400).send({ error: "Request body must be a JSON object" });
334
+ }
207
335
  const item = createItem(storage, resource, req.body);
208
336
  return reply.status(201).send(expandItems(item, req.query, resource, storage));
209
337
  });
@@ -214,14 +342,24 @@ var registerItemRoutes = (server, storage, resource, base) => {
214
342
  server.get(`${base}/:id`, (req, reply) => {
215
343
  const item = findById(storage.getCollection(resource) ?? [], req.params.id);
216
344
  if (!item) return reply.status(404).send({ error: "Not found" });
217
- return expandItems(item, req.query, resource, storage);
345
+ const fields = (req.query["_fields"] ?? "").split(",").map((f) => f.trim()).filter(Boolean);
346
+ return projectFields(
347
+ embedItems(expandItems(item, req.query, resource, storage), req.query, resource, storage),
348
+ fields
349
+ );
218
350
  });
219
351
  server.put(`${base}/:id`, (req, reply) => {
352
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
353
+ return reply.status(400).send({ error: "Request body must be a JSON object" });
354
+ }
220
355
  const item = replaceItem(storage, resource, req.params.id, req.body);
221
356
  if (!item) return reply.status(404).send({ error: "Not found" });
222
357
  return expandItems(item, req.query, resource, storage);
223
358
  });
224
359
  server.patch(`${base}/:id`, (req, reply) => {
360
+ if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
361
+ return reply.status(400).send({ error: "Request body must be a JSON object" });
362
+ }
225
363
  const item = patchItem(storage, resource, req.params.id, req.body);
226
364
  if (!item) return reply.status(404).send({ error: "Not found" });
227
365
  return expandItems(item, req.query, resource, storage);
@@ -237,7 +375,9 @@ var registerItemRoutes = (server, storage, resource, base) => {
237
375
  var registerNestedRoutes = (server, storage, relations, base) => {
238
376
  for (const [child, fields] of Object.entries(relations)) {
239
377
  for (const [field, parent] of Object.entries(fields)) {
240
- server.get(`${base}/${parent}/:id/${child}`, (req, reply) => {
378
+ const collectionPath = `${base}/${parent}/:id/${child}`;
379
+ const itemPath = `${base}/${parent}/:id/${child}/:childId`;
380
+ server.get(collectionPath, (req, reply) => {
241
381
  const parentCollection = storage.getCollection(parent) ?? [];
242
382
  const parentItem = findById(parentCollection, req.params.id);
243
383
  if (!parentItem) return reply.status(404).send({ error: "Not found" });
@@ -246,6 +386,16 @@ var registerNestedRoutes = (server, storage, relations, base) => {
246
386
  );
247
387
  return children;
248
388
  });
389
+ server.get(itemPath, (req, reply) => {
390
+ const parentCollection = storage.getCollection(parent) ?? [];
391
+ const parentItem = findById(parentCollection, req.params.id);
392
+ if (!parentItem) return reply.status(404).send({ error: "Not found" });
393
+ const childItem = (storage.getCollection(child) ?? []).find(
394
+ (item) => String(item[field]) === req.params.id && String(item["id"]) === req.params.childId
395
+ );
396
+ if (!childItem) return reply.status(404).send({ error: "Not found" });
397
+ return childItem;
398
+ });
249
399
  }
250
400
  }
251
401
  };
@@ -370,6 +520,30 @@ curl ${host}${base}/${parent}/1/${child}`);
370
520
  examples.push(`# Pageable envelope
371
521
  curl "${host}${base}/${firstCol}?_page=2"`);
372
522
  }
523
+ const firstParentRel = Object.entries(relations).find(
524
+ ([, fields]) => Object.values(fields).includes(firstCol ?? "")
525
+ );
526
+ if (firstCol) {
527
+ examples.push(
528
+ `# Project fields with ?_fields
529
+ curl "${host}${base}/${firstCol}?_fields=id,name"`
530
+ );
531
+ }
532
+ if (firstParentRel && firstCol) {
533
+ const [childName] = firstParentRel;
534
+ examples.push(
535
+ `# Embed child collection with ?_embed
536
+ curl "${host}${base}/${firstCol}/1?_embed=${childName}"`
537
+ );
538
+ }
539
+ if (options.snapshot) {
540
+ examples.push(
541
+ `# Snapshot endpoints
542
+ curl ${host}/_snapshot
543
+ curl -X POST ${host}/_snapshot/save
544
+ curl -X POST ${host}/_snapshot/reset`
545
+ );
546
+ }
373
547
  const highlighted = examples.map((e) => e.replace(/^(#.+)$/gm, '<span class="cm">$1</span>')).join("\n\n");
374
548
  return `<pre>${highlighted}</pre>`;
375
549
  }
@@ -384,6 +558,7 @@ function generateAboutHtml(storage, options) {
384
558
  if (options.delay > 0) modes.push(badge(`delay \xB7 ${options.delay}ms`, "#fb923c", "#fb923c18"));
385
559
  if (options.pageable.enabled)
386
560
  modes.push(badge(`pageable \xB7 limit ${options.pageable.limit}`, "#34d399", "#34d39918"));
561
+ if (options.snapshot) modes.push(badge("snapshot", "#c084fc", "#c084fc18"));
387
562
  const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
388
563
  const nestedRows = [];
389
564
  for (const [child, fields] of Object.entries(relations)) {
@@ -403,6 +578,18 @@ function generateAboutHtml(storage, options) {
403
578
  </summary>
404
579
  <table><tbody>${nestedRows.join("")}</tbody></table>
405
580
  </details>` : "";
581
+ const snapshotAccordion = options.snapshot ? `
582
+ <details class="resource-card nested-card">
583
+ <summary>
584
+ <span class="resource-name">/_snapshot</span>
585
+ <span class="route-count">3 routes</span>
586
+ </summary>
587
+ <table><tbody>
588
+ ${endpointRow("GET", "/_snapshot", "Returns metadata of the current snapshot: <code>savedAt</code> and item counts per collection.")}
589
+ ${endpointRow("POST", "/_snapshot/save", "Replaces the stored snapshot with the current database state.")}
590
+ ${endpointRow("POST", "/_snapshot/reset", "Restores the database to the last saved snapshot and persists to disk.")}
591
+ </tbody></table>
592
+ </details>` : "";
406
593
  const paginationDesc = options.pageable.enabled ? `Pageable mode active \u2014 default limit <code>${options.pageable.limit}</code>. Response wrapped in <code>{ data, pagination }</code>.` : `Returns the requested slice. <code>X-Total-Count</code> header reflects the total before pagination.`;
407
594
  return `<!DOCTYPE html>
408
595
  <html lang="en">
@@ -551,6 +738,7 @@ function generateAboutHtml(storage, options) {
551
738
  <div class="endpoints-grid">
552
739
  ${accordions}
553
740
  ${nestedAccordion}
741
+ ${snapshotAccordion}
554
742
  </div>
555
743
 
556
744
  <h2>Query Parameters</h2>
@@ -559,9 +747,17 @@ function generateAboutHtml(storage, options) {
559
747
  <thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
560
748
  <tbody>
561
749
  <tr><td><code>?field=value</code></td><td><code>?name=Ana&amp;role=admin</code></td><td>Filter by any field. Multiple params are ANDed.</td></tr>
750
+ <tr><td><code>?field_gte / _lte</code></td><td><code>?price_gte=10&amp;price_lte=50</code></td><td>Numeric or lexicographic range. Works with any comparable field.</td></tr>
751
+ <tr><td><code>?field_ne</code></td><td><code>?status_ne=inactive</code></td><td>Exclude items where the field equals the value.</td></tr>
752
+ <tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
753
+ <tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
754
+ <tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
755
+ <tr><td><code>?_q</code></td><td><code>?_q=ana</code></td><td>Full-text search across all scalar fields (case-insensitive substring).</td></tr>
562
756
  <tr><td><code>?_sort &amp; ?_order</code></td><td><code>?_sort=name&amp;_order=desc</code></td><td>Sort by field. <code>_order</code>: <code>asc</code> (default) or <code>desc</code>.</td></tr>
563
757
  <tr><td><code>?_page &amp; ?_limit</code></td><td><code>?_page=2&amp;_limit=10</code></td><td>${paginationDesc}</td></tr>
564
758
  <tr><td><code>?_expand</code></td><td><code>?_expand=user</code></td><td>Embed a related parent object inline. Requires <code>_rel</code> in the YAML file.</td></tr>
759
+ <tr><td><code>?_embed</code></td><td><code>?_embed=posts</code></td><td>Embed child collections into each parent item. Requires <code>_rel</code> in the YAML file.</td></tr>
760
+ <tr><td><code>?_fields</code></td><td><code>?_fields=id,name</code></td><td>Return only the specified fields. Applied last \u2014 can include embedded/expanded keys.</td></tr>
565
761
  </tbody>
566
762
  </table>
567
763
  </div>
@@ -585,11 +781,51 @@ function registerAboutRoute(server, storage, options) {
585
781
  });
586
782
  }
587
783
 
784
+ // src/router/routes/snapshot.routes.ts
785
+ function registerSnapshotRoutes(server, storage) {
786
+ server.get("/_snapshot", (_req, reply) => {
787
+ const { data, savedAt } = storage.getSnapshot();
788
+ return reply.send({
789
+ savedAt: savedAt.toISOString(),
790
+ collections: Object.fromEntries(
791
+ Object.entries(data).map(([name, items]) => [name, items.length])
792
+ )
793
+ });
794
+ });
795
+ server.post("/_snapshot/save", (_req, reply) => {
796
+ storage.saveSnapshot();
797
+ const { data, savedAt } = storage.getSnapshot();
798
+ return reply.send({
799
+ message: "Snapshot saved",
800
+ savedAt: savedAt.toISOString(),
801
+ collections: Object.fromEntries(
802
+ Object.entries(data).map(([name, items]) => [name, items.length])
803
+ )
804
+ });
805
+ });
806
+ server.post("/_snapshot/reset", (_req, reply) => {
807
+ storage.resetToSnapshot();
808
+ const { data, savedAt } = storage.getSnapshot();
809
+ return reply.send({
810
+ message: "Database restored to snapshot",
811
+ savedAt: savedAt.toISOString(),
812
+ collections: Object.fromEntries(
813
+ Object.entries(data).map(([name, items]) => [name, items.length])
814
+ )
815
+ });
816
+ });
817
+ }
818
+
588
819
  // src/server/createServer.ts
589
820
  var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
590
821
  async function createServer(storage, options) {
591
822
  const server = Fastify();
592
823
  await server.register(cors);
824
+ server.setErrorHandler((err, _req, reply) => {
825
+ const status = err.statusCode ?? 500;
826
+ const message = status < 500 ? err.message || "Request error" : "Internal server error";
827
+ reply.status(status).send({ error: message });
828
+ });
593
829
  if (options.readonly) {
594
830
  server.addHook("onRequest", (_req, reply, done) => {
595
831
  if (MUTATING_METHODS.has(_req.method)) {
@@ -604,6 +840,7 @@ async function createServer(storage, options) {
604
840
  });
605
841
  }
606
842
  registerAboutRoute(server, storage, options);
843
+ if (options.snapshot) registerSnapshotRoutes(server, storage);
607
844
  registerResourceRoutes(server, storage, options);
608
845
  return server;
609
846
  }
@@ -624,6 +861,11 @@ var serverOptionsSchema = z.object({
624
861
  base: z.string().default("").transform((v) => v && !v.startsWith("/") ? `/${v}` : v),
625
862
  /** When `true`, the server reloads the YAML file automatically on disk changes. */
626
863
  watch: z.boolean().default(false),
864
+ /**
865
+ * When `true`, saves a snapshot of the initial database state on startup.
866
+ * Exposes `/_snapshot` endpoints to inspect, restore or update the snapshot.
867
+ */
868
+ snapshot: z.boolean().default(false),
627
869
  /** When `true`, all mutating requests (POST, PUT, PATCH, DELETE) are rejected with 405. */
628
870
  readonly: z.boolean().default(false),
629
871
  /** Milliseconds to delay every response, simulating network latency. `0` = disabled. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aggiovato/yrest",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Zero-config REST API mock server powered by a YAML file",
5
5
  "keywords": [
6
6
  "yaml",