@yrest/cli 0.11.1 → 0.11.2

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 CHANGED
@@ -10,7 +10,7 @@
10
10
  [![CI](https://github.com/aggiovato/yRest/actions/workflows/ci.yml/badge.svg)](https://github.com/aggiovato/yRest/actions)
11
11
  [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
12
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
13
- [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.11.1)](https://socket.dev/npm/package/@yrest/cli)
13
+ [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.11.2)](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
 
@@ -241,26 +241,59 @@ posts:
241
241
 
242
242
  Each top-level key becomes a resource with full CRUD endpoints.
243
243
 
244
+ Alternatively, collections can be grouped under a `_data` block:
245
+
246
+ ```yaml
247
+ _data:
248
+ users:
249
+ - id: 1
250
+ name: Ana
251
+ posts:
252
+ - id: 1
253
+ title: First post
254
+ userId: 1
255
+ ```
256
+
257
+ 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
+
244
259
  ---
245
260
 
246
261
  ## Field annotations (`_schema`)
247
262
 
248
263
  Add a `_schema` block to `db.yml` to declare field-level metadata per collection. Used by the OpenAPI generator to produce accurate schemas — has no effect on runtime CRUD behavior.
249
264
 
265
+ All keys inside `_schema` field objects use the `_` prefix convention. Two shorthand strings are also accepted at the field level:
266
+
250
267
  ```yaml
251
268
  _schema:
252
269
  users:
253
- name: required # shorthand: marks field as required
270
+ name: required # shorthand → { _required: true }
254
271
  email:
255
- required: true
256
- format: email
272
+ _required: true
273
+ _type: string
274
+ _format: email
275
+ _minLength: 5
276
+ _maxLength: 100
257
277
  age:
258
- type: integer
278
+ _type: integer
279
+ _minimum: 0
280
+ _maximum: 150
281
+ _nullable: true
259
282
  role:
260
- type: string
261
- enum: [admin, editor, viewer]
262
- default: viewer
263
- description: User role
283
+ _type: string
284
+ _enum: [admin, editor, viewer]
285
+ _default: viewer
286
+ _description: User role
287
+ tags:
288
+ _type: array
289
+ _items:
290
+ _type: string
291
+ _minItems: 1
292
+ _uniqueItems: true
293
+ createdAt:
294
+ _type: string
295
+ _format: date-time
296
+ _readOnly: true
264
297
 
265
298
  users:
266
299
  - id: 1
@@ -270,14 +303,53 @@ users:
270
303
  role: admin
271
304
  ```
272
305
 
273
- | Key | Type | Description |
274
- | ------------- | --------- | ----------------------------------------------------------------------------------------- |
275
- | `required` | `boolean` | Marks the field as required in the OpenAPI schema |
276
- | `type` | `string` | Overrides the inferred type (`string`, `integer`, `number`, `boolean`, `array`, `object`) |
277
- | `format` | `string` | OpenAPI format hint (`email`, `date`, `date-time`, `uuid`, `uri`, …) |
278
- | `enum` | `array` | Restricts the field to a fixed set of values |
279
- | `description` | `string` | Field description included in the OpenAPI schema |
280
- | `default` | `any` | Default value shown in the OpenAPI schema |
306
+ **Core**
307
+
308
+ | Key | Type | Description |
309
+ | -------------- | --------- | ----------------------------------------------------------------------------------------- |
310
+ | `_required` | `boolean` | Marks the field as required in the OpenAPI schema |
311
+ | `_type` | `string` | Overrides the inferred type (`string`, `integer`, `number`, `boolean`, `array`, `object`) |
312
+ | `_format` | `string` | OpenAPI format hint (`email`, `date`, `date-time`, `uuid`, `uri`, …) |
313
+ | `_enum` | `array` | Restricts the field to a fixed set of values |
314
+ | `_description` | `string` | Field description included in the OpenAPI schema |
315
+ | `_default` | `any` | Default value shown in the OpenAPI schema |
316
+ | `_example` | `any` | Example value shown in the OpenAPI schema |
317
+ | `_nullable` | `boolean` | Allows the field to be `null` |
318
+
319
+ **String constraints**
320
+
321
+ | Key | Type | Description |
322
+ | ------------ | -------- | ------------------------------------ |
323
+ | `_minLength` | `number` | Minimum string length |
324
+ | `_maxLength` | `number` | Maximum string length |
325
+ | `_pattern` | `string` | Regex pattern the value must satisfy |
326
+
327
+ **Number / integer constraints**
328
+
329
+ | Key | Type | Description |
330
+ | ------------------- | -------- | ----------------------------- |
331
+ | `_minimum` | `number` | Inclusive lower bound |
332
+ | `_maximum` | `number` | Inclusive upper bound |
333
+ | `_exclusiveMinimum` | `number` | Exclusive lower bound |
334
+ | `_exclusiveMaximum` | `number` | Exclusive upper bound |
335
+ | `_multipleOf` | `number` | Value must be a multiple of N |
336
+
337
+ **Array constraints**
338
+
339
+ | Key | Type | Description |
340
+ | -------------- | --------- | ------------------------------------------------------------ |
341
+ | `_minItems` | `number` | Minimum number of array items |
342
+ | `_maxItems` | `number` | Maximum number of array items |
343
+ | `_uniqueItems` | `boolean` | All items must be unique |
344
+ | `_items` | `object` | Schema for array items — accepts `_type`, `_format`, `_enum` |
345
+
346
+ **OpenAPI meta**
347
+
348
+ | Key | Type | Description |
349
+ | ------------- | --------- | --------------------------------------- |
350
+ | `_deprecated` | `boolean` | Marks the field as deprecated |
351
+ | `_readOnly` | `boolean` | Field is read-only (excluded from POST) |
352
+ | `_writeOnly` | `boolean` | Field is write-only (excluded from GET) |
281
353
 
282
354
  Fields not listed in `_schema` are inferred from the first items in the collection and treated as optional. `_schema` itself is excluded from the generated REST resources.
283
355
 
@@ -722,14 +794,14 @@ when:
722
794
  - body.role: superadmin
723
795
  ```
724
796
 
725
- Condition keys use dot-notation to address request data:
797
+ Condition keys use dot-notation to address request data. Both bare and `_`-prefixed forms are accepted:
726
798
 
727
- | Prefix | Example | Resolves to |
728
- | ----------- | ------------------- | -------------------------- |
729
- | `body.X` | `body.email` | `req.body.email` |
730
- | `params.X` | `params.id` | `req.params.id` |
731
- | `query.X` | `query.page` | `req.query.page` |
732
- | `headers.X` | `headers.x-api-key` | `req.headers["x-api-key"]` |
799
+ | Prefix | Example | Resolves to |
800
+ | -------------------------- | ------------------- | -------------------------- |
801
+ | `body.X` / `_body.X` | `body.email` | `req.body.email` |
802
+ | `params.X` / `_params.X` | `params.id` | `req.params.id` |
803
+ | `query.X` / `_query.X` | `query.page` | `req.query.page` |
804
+ | `headers.X` / `_headers.X` | `headers.x-api-key` | `req.headers["x-api-key"]` |
733
805
 
734
806
  Field operator suffixes (`_ne`, `_like`, `_start`, `_regex`, `_gte`, `_lte`) work on condition keys exactly as they do on query params:
735
807
 
package/dist/cli/index.js CHANGED
@@ -717,6 +717,7 @@ function parseSchema(raw) {
717
717
  }
718
718
  return result;
719
719
  }
720
+ var VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
720
721
  function normaliseFieldDef(value) {
721
722
  if (value === "required") return { required: true };
722
723
  if (value === "optional") return { required: false };
@@ -724,15 +725,60 @@ function normaliseFieldDef(value) {
724
725
  const v = value;
725
726
  const def = {};
726
727
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
727
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
728
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
728
729
  def.type = v["_type"];
729
730
  if (typeof v["_format"] === "string") def.format = v["_format"];
730
731
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
731
732
  if (typeof v["_description"] === "string") def.description = v["_description"];
732
733
  if (v["_default"] !== void 0) def.default = v["_default"];
734
+ if (v["_example"] !== void 0) def.example = v["_example"];
735
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
736
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
737
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
738
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
739
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
740
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
741
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
742
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
743
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
744
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
745
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
746
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
747
+ def.uniqueItems = v["_uniqueItems"];
748
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
749
+ const items = v["_items"];
750
+ const itemsDef = {};
751
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
752
+ itemsDef.type = items["_type"];
753
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
754
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
755
+ def.items = itemsDef;
756
+ }
757
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
758
+ def.deprecated = v["_deprecated"];
759
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
760
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
761
+ def.writeOnly = v["_writeOnly"];
733
762
  return def;
734
763
  }
735
764
 
765
+ // src/storage/parseData.ts
766
+ var RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
767
+ function parseData(raw) {
768
+ const block = raw["_data"];
769
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
770
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
771
+ ) : {};
772
+ const fromRoot = Object.fromEntries(
773
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
774
+ );
775
+ return { ...fromBlock, ...fromRoot };
776
+ }
777
+ function hasDataBlock(raw) {
778
+ const d = raw["_data"];
779
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
780
+ }
781
+
736
782
  // src/utils/deepCopy.ts
737
783
  function deepCopyData(source) {
738
784
  return Object.fromEntries(
@@ -798,13 +844,11 @@ function serializeScenario(s) {
798
844
  function createYrestStorage(filePath) {
799
845
  const absPath = (0, import_node_path2.resolve)(filePath);
800
846
  const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
801
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
802
847
  const relations = parseRelations(raw["_rel"]);
803
848
  const routes = parseRoutes(raw["_routes"]);
804
849
  const schema = parseSchema(raw["_schema"]);
805
- const data = Object.fromEntries(
806
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
807
- );
850
+ const dataBlock = hasDataBlock(raw);
851
+ const data = parseData(raw);
808
852
  let snapshot = {
809
853
  data: deepCopyData(data),
810
854
  relations: { ...relations },
@@ -833,7 +877,11 @@ function createYrestStorage(filePath) {
833
877
  const payload = {};
834
878
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
835
879
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
836
- Object.assign(payload, data);
880
+ if (dataBlock) {
881
+ payload._data = data;
882
+ } else {
883
+ Object.assign(payload, data);
884
+ }
837
885
  const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto.randomUUID)()}.tmp`);
838
886
  (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml.stringify)(payload), "utf8");
839
887
  (0, import_node_fs2.renameSync)(tmp, absPath);
@@ -841,9 +889,7 @@ function createYrestStorage(filePath) {
841
889
  reload() {
842
890
  const fresh = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
843
891
  const freshRelations = parseRelations(fresh["_rel"]);
844
- const freshData = Object.fromEntries(
845
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
846
- );
892
+ const freshData = parseData(fresh);
847
893
  for (const key of Object.keys(data)) delete data[key];
848
894
  Object.assign(data, freshData);
849
895
  for (const key of Object.keys(relations)) delete relations[key];
@@ -2403,15 +2449,19 @@ function resolveRequestPath(dotPath, req) {
2403
2449
  const [root, ...rest] = dotPath.split(".");
2404
2450
  let value;
2405
2451
  switch (root) {
2452
+ case "_body":
2406
2453
  case "body":
2407
2454
  value = req.body;
2408
2455
  break;
2456
+ case "_params":
2409
2457
  case "params":
2410
2458
  value = req.params;
2411
2459
  break;
2460
+ case "_query":
2412
2461
  case "query":
2413
2462
  value = req.query;
2414
2463
  break;
2464
+ case "_headers":
2415
2465
  case "headers":
2416
2466
  value = req.headers;
2417
2467
  break;
@@ -2703,6 +2753,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2703
2753
  if (def?.description) prop.description = def.description;
2704
2754
  if (def?.enum) prop.enum = def.enum;
2705
2755
  if (def?.default !== void 0) prop.default = def.default;
2756
+ if (def?.example !== void 0) prop.example = def.example;
2757
+ if (def?.nullable) prop.nullable = def.nullable;
2758
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2759
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2760
+ if (def?.pattern) prop.pattern = def.pattern;
2761
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2762
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2763
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2764
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2765
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2766
+ if (def?.items) prop.items = def.items;
2767
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2768
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2769
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2770
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2771
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2772
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2706
2773
  properties[field] = prop;
2707
2774
  if (def?.required === true) required.push(field);
2708
2775
  }
@@ -3245,6 +3312,14 @@ async function loadHandlers(filePath) {
3245
3312
  }
3246
3313
 
3247
3314
  // src/cli/commands/serve.ts
3315
+ function buildServeOptions(file, fileConfig, cliOverrides, fileArgIsExplicit = false) {
3316
+ return yrestOptionsSchema.parse({
3317
+ file,
3318
+ ...fileConfig,
3319
+ ...fileArgIsExplicit ? { file } : {},
3320
+ ...cliOverrides
3321
+ });
3322
+ }
3248
3323
  function registerServe(program2) {
3249
3324
  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(
3250
3325
  "-d, --delay <ms>",
@@ -3268,13 +3343,7 @@ function registerServe(program2) {
3268
3343
  const cliOverrides = Object.fromEntries(
3269
3344
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3270
3345
  );
3271
- const merged = {
3272
- file,
3273
- ...fileConfig,
3274
- ...cmd.args.length > 0 ? { file } : {},
3275
- ...cliOverrides
3276
- };
3277
- const options = yrestOptionsSchema.parse(merged);
3346
+ const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3278
3347
  let storage;
3279
3348
  try {
3280
3349
  storage = createYrestStorage(options.file);
@@ -690,6 +690,7 @@ function parseSchema(raw) {
690
690
  }
691
691
  return result;
692
692
  }
693
+ var VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
693
694
  function normaliseFieldDef(value) {
694
695
  if (value === "required") return { required: true };
695
696
  if (value === "optional") return { required: false };
@@ -697,15 +698,60 @@ function normaliseFieldDef(value) {
697
698
  const v = value;
698
699
  const def = {};
699
700
  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"]))
701
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
701
702
  def.type = v["_type"];
702
703
  if (typeof v["_format"] === "string") def.format = v["_format"];
703
704
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
704
705
  if (typeof v["_description"] === "string") def.description = v["_description"];
705
706
  if (v["_default"] !== void 0) def.default = v["_default"];
707
+ if (v["_example"] !== void 0) def.example = v["_example"];
708
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
709
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
710
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
711
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
712
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
713
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
714
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
715
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
716
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
717
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
718
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
719
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
720
+ def.uniqueItems = v["_uniqueItems"];
721
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
722
+ const items = v["_items"];
723
+ const itemsDef = {};
724
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
725
+ itemsDef.type = items["_type"];
726
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
727
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
728
+ def.items = itemsDef;
729
+ }
730
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
731
+ def.deprecated = v["_deprecated"];
732
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
733
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
734
+ def.writeOnly = v["_writeOnly"];
706
735
  return def;
707
736
  }
708
737
 
738
+ // src/storage/parseData.ts
739
+ var RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
740
+ function parseData(raw) {
741
+ const block = raw["_data"];
742
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
743
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
744
+ ) : {};
745
+ const fromRoot = Object.fromEntries(
746
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
747
+ );
748
+ return { ...fromBlock, ...fromRoot };
749
+ }
750
+ function hasDataBlock(raw) {
751
+ const d = raw["_data"];
752
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
753
+ }
754
+
709
755
  // src/utils/deepCopy.ts
710
756
  function deepCopyData(source) {
711
757
  return Object.fromEntries(
@@ -771,13 +817,11 @@ function serializeScenario(s) {
771
817
  function createYrestStorage(filePath) {
772
818
  const absPath = resolve2(filePath);
773
819
  const raw = parse(readFileSync(absPath, "utf8")) ?? {};
774
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
775
820
  const relations = parseRelations(raw["_rel"]);
776
821
  const routes = parseRoutes(raw["_routes"]);
777
822
  const schema = parseSchema(raw["_schema"]);
778
- const data = Object.fromEntries(
779
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
780
- );
823
+ const dataBlock = hasDataBlock(raw);
824
+ const data = parseData(raw);
781
825
  let snapshot = {
782
826
  data: deepCopyData(data),
783
827
  relations: { ...relations },
@@ -806,7 +850,11 @@ function createYrestStorage(filePath) {
806
850
  const payload = {};
807
851
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
808
852
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
809
- Object.assign(payload, data);
853
+ if (dataBlock) {
854
+ payload._data = data;
855
+ } else {
856
+ Object.assign(payload, data);
857
+ }
810
858
  const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID()}.tmp`);
811
859
  writeFileSync2(tmp, stringify(payload), "utf8");
812
860
  renameSync(tmp, absPath);
@@ -814,9 +862,7 @@ function createYrestStorage(filePath) {
814
862
  reload() {
815
863
  const fresh = parse(readFileSync(absPath, "utf8")) ?? {};
816
864
  const freshRelations = parseRelations(fresh["_rel"]);
817
- const freshData = Object.fromEntries(
818
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
819
- );
865
+ const freshData = parseData(fresh);
820
866
  for (const key of Object.keys(data)) delete data[key];
821
867
  Object.assign(data, freshData);
822
868
  for (const key of Object.keys(relations)) delete relations[key];
@@ -2376,15 +2422,19 @@ function resolveRequestPath(dotPath, req) {
2376
2422
  const [root, ...rest] = dotPath.split(".");
2377
2423
  let value;
2378
2424
  switch (root) {
2425
+ case "_body":
2379
2426
  case "body":
2380
2427
  value = req.body;
2381
2428
  break;
2429
+ case "_params":
2382
2430
  case "params":
2383
2431
  value = req.params;
2384
2432
  break;
2433
+ case "_query":
2385
2434
  case "query":
2386
2435
  value = req.query;
2387
2436
  break;
2437
+ case "_headers":
2388
2438
  case "headers":
2389
2439
  value = req.headers;
2390
2440
  break;
@@ -2676,6 +2726,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2676
2726
  if (def?.description) prop.description = def.description;
2677
2727
  if (def?.enum) prop.enum = def.enum;
2678
2728
  if (def?.default !== void 0) prop.default = def.default;
2729
+ if (def?.example !== void 0) prop.example = def.example;
2730
+ if (def?.nullable) prop.nullable = def.nullable;
2731
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2732
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2733
+ if (def?.pattern) prop.pattern = def.pattern;
2734
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2735
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2736
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2737
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2738
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2739
+ if (def?.items) prop.items = def.items;
2740
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2741
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2742
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2743
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2744
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2745
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2679
2746
  properties[field] = prop;
2680
2747
  if (def?.required === true) required.push(field);
2681
2748
  }
@@ -3218,6 +3285,14 @@ async function loadHandlers(filePath) {
3218
3285
  }
3219
3286
 
3220
3287
  // src/cli/commands/serve.ts
3288
+ function buildServeOptions(file, fileConfig, cliOverrides, fileArgIsExplicit = false) {
3289
+ return yrestOptionsSchema.parse({
3290
+ file,
3291
+ ...fileConfig,
3292
+ ...fileArgIsExplicit ? { file } : {},
3293
+ ...cliOverrides
3294
+ });
3295
+ }
3221
3296
  function registerServe(program2) {
3222
3297
  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
3298
  "-d, --delay <ms>",
@@ -3241,13 +3316,7 @@ function registerServe(program2) {
3241
3316
  const cliOverrides = Object.fromEntries(
3242
3317
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3243
3318
  );
3244
- const merged = {
3245
- file,
3246
- ...fileConfig,
3247
- ...cmd.args.length > 0 ? { file } : {},
3248
- ...cliOverrides
3249
- };
3250
- const options = yrestOptionsSchema.parse(merged);
3319
+ const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3251
3320
  let storage;
3252
3321
  try {
3253
3322
  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.
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.
package/dist/index.js CHANGED
@@ -174,18 +174,48 @@ function normaliseFieldDef(value) {
174
174
  const v = value;
175
175
  const def = {};
176
176
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
177
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
177
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
178
178
  def.type = v["_type"];
179
179
  if (typeof v["_format"] === "string") def.format = v["_format"];
180
180
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
181
181
  if (typeof v["_description"] === "string") def.description = v["_description"];
182
182
  if (v["_default"] !== void 0) def.default = v["_default"];
183
+ if (v["_example"] !== void 0) def.example = v["_example"];
184
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
185
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
186
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
187
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
188
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
189
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
190
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
191
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
192
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
193
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
194
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
195
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
196
+ def.uniqueItems = v["_uniqueItems"];
197
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
198
+ const items = v["_items"];
199
+ const itemsDef = {};
200
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
201
+ itemsDef.type = items["_type"];
202
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
203
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
204
+ def.items = itemsDef;
205
+ }
206
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
207
+ def.deprecated = v["_deprecated"];
208
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
209
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
210
+ def.writeOnly = v["_writeOnly"];
183
211
  return def;
184
212
  }
213
+ var VALID_TYPES;
185
214
  var init_parseSchema = __esm({
186
215
  "src/storage/parseSchema.ts"() {
187
216
  "use strict";
188
217
  init_cjs_shims();
218
+ VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
189
219
  }
190
220
  });
191
221
 
@@ -256,6 +286,30 @@ var init_parseRoutes = __esm({
256
286
  }
257
287
  });
258
288
 
289
+ // src/storage/parseData.ts
290
+ function parseData(raw) {
291
+ const block = raw["_data"];
292
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
293
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
294
+ ) : {};
295
+ const fromRoot = Object.fromEntries(
296
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
297
+ );
298
+ return { ...fromBlock, ...fromRoot };
299
+ }
300
+ function hasDataBlock(raw) {
301
+ const d = raw["_data"];
302
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
303
+ }
304
+ var RESERVED;
305
+ var init_parseData = __esm({
306
+ "src/storage/parseData.ts"() {
307
+ "use strict";
308
+ init_cjs_shims();
309
+ RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
310
+ }
311
+ });
312
+
259
313
  // src/storage/yrestStorage.ts
260
314
  var yrestStorage_exports = {};
261
315
  __export(yrestStorage_exports, {
@@ -318,13 +372,11 @@ function serializeScenario(s) {
318
372
  function createYrestStorage(filePath) {
319
373
  const absPath = (0, import_node_path2.resolve)(filePath);
320
374
  const raw = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
321
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
322
375
  const relations = parseRelations(raw["_rel"]);
323
376
  const routes = parseRoutes(raw["_routes"]);
324
377
  const schema = parseSchema(raw["_schema"]);
325
- const data = Object.fromEntries(
326
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
327
- );
378
+ const dataBlock = hasDataBlock(raw);
379
+ const data = parseData(raw);
328
380
  let snapshot = {
329
381
  data: deepCopyData(data),
330
382
  relations: { ...relations },
@@ -353,7 +405,11 @@ function createYrestStorage(filePath) {
353
405
  const payload = {};
354
406
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
355
407
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
356
- Object.assign(payload, data);
408
+ if (dataBlock) {
409
+ payload._data = data;
410
+ } else {
411
+ Object.assign(payload, data);
412
+ }
357
413
  const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
358
414
  (0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml3.stringify)(payload), "utf8");
359
415
  (0, import_node_fs3.renameSync)(tmp, absPath);
@@ -361,9 +417,7 @@ function createYrestStorage(filePath) {
361
417
  reload() {
362
418
  const fresh = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
363
419
  const freshRelations = parseRelations(fresh["_rel"]);
364
- const freshData = Object.fromEntries(
365
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
366
- );
420
+ const freshData = parseData(fresh);
367
421
  for (const key of Object.keys(data)) delete data[key];
368
422
  Object.assign(data, freshData);
369
423
  for (const key of Object.keys(relations)) delete relations[key];
@@ -401,6 +455,7 @@ var init_yrestStorage = __esm({
401
455
  init_parseRelations();
402
456
  init_parseRoutes();
403
457
  init_parseSchema();
458
+ init_parseData();
404
459
  init_deepCopy();
405
460
  }
406
461
  });
@@ -2059,15 +2114,19 @@ function resolveRequestPath(dotPath, req) {
2059
2114
  const [root, ...rest] = dotPath.split(".");
2060
2115
  let value;
2061
2116
  switch (root) {
2117
+ case "_body":
2062
2118
  case "body":
2063
2119
  value = req.body;
2064
2120
  break;
2121
+ case "_params":
2065
2122
  case "params":
2066
2123
  value = req.params;
2067
2124
  break;
2125
+ case "_query":
2068
2126
  case "query":
2069
2127
  value = req.query;
2070
2128
  break;
2129
+ case "_headers":
2071
2130
  case "headers":
2072
2131
  value = req.headers;
2073
2132
  break;
@@ -2368,6 +2427,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2368
2427
  if (def?.description) prop.description = def.description;
2369
2428
  if (def?.enum) prop.enum = def.enum;
2370
2429
  if (def?.default !== void 0) prop.default = def.default;
2430
+ if (def?.example !== void 0) prop.example = def.example;
2431
+ if (def?.nullable) prop.nullable = def.nullable;
2432
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2433
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2434
+ if (def?.pattern) prop.pattern = def.pattern;
2435
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2436
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2437
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2438
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2439
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2440
+ if (def?.items) prop.items = def.items;
2441
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2442
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2443
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2444
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2445
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2446
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2371
2447
  properties[field] = prop;
2372
2448
  if (def?.required === true) required.push(field);
2373
2449
  }
@@ -2871,12 +2947,12 @@ function buildOptions(opts) {
2871
2947
  }
2872
2948
  function createInMemoryStorage(data) {
2873
2949
  const raw = data;
2874
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2950
+ const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2875
2951
  const relations = parseRelations(raw["_rel"]);
2876
2952
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
2877
2953
  const schema = parseSchema(raw["_schema"]);
2878
2954
  const collections = Object.fromEntries(
2879
- Object.entries(raw).filter(([k]) => !RESERVED.has(k))
2955
+ Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
2880
2956
  );
2881
2957
  let snapshot = {
2882
2958
  data: deepCopyData(collections),
package/dist/index.mjs CHANGED
@@ -151,18 +151,48 @@ function normaliseFieldDef(value) {
151
151
  const v = value;
152
152
  const def = {};
153
153
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
154
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
154
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
155
155
  def.type = v["_type"];
156
156
  if (typeof v["_format"] === "string") def.format = v["_format"];
157
157
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
158
158
  if (typeof v["_description"] === "string") def.description = v["_description"];
159
159
  if (v["_default"] !== void 0) def.default = v["_default"];
160
+ if (v["_example"] !== void 0) def.example = v["_example"];
161
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
162
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
163
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
164
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
165
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
166
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
167
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
168
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
169
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
170
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
171
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
172
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
173
+ def.uniqueItems = v["_uniqueItems"];
174
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
175
+ const items = v["_items"];
176
+ const itemsDef = {};
177
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
178
+ itemsDef.type = items["_type"];
179
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
180
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
181
+ def.items = itemsDef;
182
+ }
183
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
184
+ def.deprecated = v["_deprecated"];
185
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
186
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
187
+ def.writeOnly = v["_writeOnly"];
160
188
  return def;
161
189
  }
190
+ var VALID_TYPES;
162
191
  var init_parseSchema = __esm({
163
192
  "src/storage/parseSchema.ts"() {
164
193
  "use strict";
165
194
  init_esm_shims();
195
+ VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
166
196
  }
167
197
  });
168
198
 
@@ -233,6 +263,30 @@ var init_parseRoutes = __esm({
233
263
  }
234
264
  });
235
265
 
266
+ // src/storage/parseData.ts
267
+ function parseData(raw) {
268
+ const block = raw["_data"];
269
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
270
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
271
+ ) : {};
272
+ const fromRoot = Object.fromEntries(
273
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
274
+ );
275
+ return { ...fromBlock, ...fromRoot };
276
+ }
277
+ function hasDataBlock(raw) {
278
+ const d = raw["_data"];
279
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
280
+ }
281
+ var RESERVED;
282
+ var init_parseData = __esm({
283
+ "src/storage/parseData.ts"() {
284
+ "use strict";
285
+ init_esm_shims();
286
+ RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
287
+ }
288
+ });
289
+
236
290
  // src/storage/yrestStorage.ts
237
291
  var yrestStorage_exports = {};
238
292
  __export(yrestStorage_exports, {
@@ -299,13 +353,11 @@ function serializeScenario(s) {
299
353
  function createYrestStorage(filePath) {
300
354
  const absPath = resolve(filePath);
301
355
  const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
302
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
303
356
  const relations = parseRelations(raw["_rel"]);
304
357
  const routes = parseRoutes(raw["_routes"]);
305
358
  const schema = parseSchema(raw["_schema"]);
306
- const data = Object.fromEntries(
307
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
308
- );
359
+ const dataBlock = hasDataBlock(raw);
360
+ const data = parseData(raw);
309
361
  let snapshot = {
310
362
  data: deepCopyData(data),
311
363
  relations: { ...relations },
@@ -334,7 +386,11 @@ function createYrestStorage(filePath) {
334
386
  const payload = {};
335
387
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
336
388
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
337
- Object.assign(payload, data);
389
+ if (dataBlock) {
390
+ payload._data = data;
391
+ } else {
392
+ Object.assign(payload, data);
393
+ }
338
394
  const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
339
395
  writeFileSync(tmp, stringify2(payload), "utf8");
340
396
  renameSync(tmp, absPath);
@@ -342,9 +398,7 @@ function createYrestStorage(filePath) {
342
398
  reload() {
343
399
  const fresh = parse2(readFileSync2(absPath, "utf8")) ?? {};
344
400
  const freshRelations = parseRelations(fresh["_rel"]);
345
- const freshData = Object.fromEntries(
346
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
347
- );
401
+ const freshData = parseData(fresh);
348
402
  for (const key of Object.keys(data)) delete data[key];
349
403
  Object.assign(data, freshData);
350
404
  for (const key of Object.keys(relations)) delete relations[key];
@@ -377,6 +431,7 @@ var init_yrestStorage = __esm({
377
431
  init_parseRelations();
378
432
  init_parseRoutes();
379
433
  init_parseSchema();
434
+ init_parseData();
380
435
  init_deepCopy();
381
436
  }
382
437
  });
@@ -2025,15 +2080,19 @@ function resolveRequestPath(dotPath, req) {
2025
2080
  const [root, ...rest] = dotPath.split(".");
2026
2081
  let value;
2027
2082
  switch (root) {
2083
+ case "_body":
2028
2084
  case "body":
2029
2085
  value = req.body;
2030
2086
  break;
2087
+ case "_params":
2031
2088
  case "params":
2032
2089
  value = req.params;
2033
2090
  break;
2091
+ case "_query":
2034
2092
  case "query":
2035
2093
  value = req.query;
2036
2094
  break;
2095
+ case "_headers":
2037
2096
  case "headers":
2038
2097
  value = req.headers;
2039
2098
  break;
@@ -2334,6 +2393,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2334
2393
  if (def?.description) prop.description = def.description;
2335
2394
  if (def?.enum) prop.enum = def.enum;
2336
2395
  if (def?.default !== void 0) prop.default = def.default;
2396
+ if (def?.example !== void 0) prop.example = def.example;
2397
+ if (def?.nullable) prop.nullable = def.nullable;
2398
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2399
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2400
+ if (def?.pattern) prop.pattern = def.pattern;
2401
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2402
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2403
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2404
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2405
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2406
+ if (def?.items) prop.items = def.items;
2407
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2408
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2409
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2410
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2411
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2412
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2337
2413
  properties[field] = prop;
2338
2414
  if (def?.required === true) required.push(field);
2339
2415
  }
@@ -2837,12 +2913,12 @@ function buildOptions(opts) {
2837
2913
  }
2838
2914
  function createInMemoryStorage(data) {
2839
2915
  const raw = data;
2840
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2916
+ const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2841
2917
  const relations = parseRelations(raw["_rel"]);
2842
2918
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
2843
2919
  const schema = parseSchema(raw["_schema"]);
2844
2920
  const collections = Object.fromEntries(
2845
- Object.entries(raw).filter(([k]) => !RESERVED.has(k))
2921
+ Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
2846
2922
  );
2847
2923
  let snapshot = {
2848
2924
  data: deepCopyData(collections),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.",
5
5
  "keywords": [
6
6
  "yrest",
@@ -60,12 +60,16 @@
60
60
  "dev": "tsup --watch",
61
61
  "test": "vitest",
62
62
  "test:run": "vitest run",
63
+ "test:unit": "vitest run tests/config tests/cli tests/storage tests/utils",
64
+ "test:integration": "vitest run tests/routes tests/openapi tests/security tests/api",
65
+ "test:e2e": "vitest run tests/e2e",
66
+ "test:preflight": "vitest run",
63
67
  "typecheck": "tsc --noEmit",
64
68
  "lint": "eslint src tests",
65
69
  "lint:fix": "eslint src tests --fix",
66
70
  "format": "prettier --write .",
67
71
  "format:check": "prettier --check .",
68
- "prepublishOnly": "npm run test:run && npm run build",
72
+ "prepublishOnly": "npm run build && npm run test:preflight",
69
73
  "version": "node scripts/update-version-badge.mjs && git add README.md"
70
74
  },
71
75
  "dependencies": {