@yrest/cli 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.12.0)](https://socket.dev/npm/package/@yrest/cli)
13
+ [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.13.0)](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
 
@@ -257,6 +257,31 @@ _data:
257
257
 
258
258
  Both formats are equivalent. You can also mix them — if the same collection name appears both inside `_data` and at the root level, the root-level entry wins. The file is persisted in whichever format it was originally written.
259
259
 
260
+ ### Data directives (`__uuid_gen`, `__fk`)
261
+
262
+ Use `__uuid_gen` and `__fk` directives to generate UUID-based IDs and cross-collection references automatically. yRest resolves all directives on first load and rewrites the file with real values — subsequent restarts are stable.
263
+
264
+ ```yaml
265
+ users:
266
+ - id: __uuid_gen:ana # generates a UUID and registers alias "ana"
267
+ name: Ana
268
+ - id: __uuid_gen:luis # generates a UUID and registers alias "luis"
269
+ name: Luis
270
+
271
+ posts:
272
+ - id: __uuid_gen # generates a UUID (no alias needed)
273
+ title: Hello World
274
+ userId: __fk.users:ana # resolves to the UUID registered as "ana" in users
275
+ ```
276
+
277
+ | Directive | Meaning |
278
+ | ----------------------- | ---------------------------------------------------------------- |
279
+ | `__uuid_gen` | Generate a UUID v4 for this field |
280
+ | `__uuid_gen:alias` | Generate a UUID and register it under `alias` in this collection |
281
+ | `__fk.collection:alias` | Resolve to the UUID registered as `alias` in `collection` |
282
+
283
+ Aliases are scoped to their collection — `__uuid_gen:ale` in `users` and `__uuid_gen:ale` in `categories` are independent.
284
+
260
285
  ---
261
286
 
262
287
  ## Field annotations (`_schema`)
package/dist/cli/index.js CHANGED
@@ -115,6 +115,7 @@ categories:
115
115
  // src/cli/commands/templates/relational.ts
116
116
  var relationalTemplate = `# yrest relational sample \u2014 blog
117
117
  # Demonstrates: many2one, one2one and many2many relationships
118
+ # Also shows __uuid_gen and __fk directives for UUID-based IDs
118
119
  # Run: npx @yrest/cli serve db.yml
119
120
  # Docs: GET http://localhost:3070/_about
120
121
 
@@ -139,33 +140,36 @@ _rel:
139
140
  _type: one2one
140
141
  _target: users
141
142
 
143
+ # __uuid_gen:alias generates a UUID and registers it under that alias.
144
+ # __fk.collection:alias resolves to the UUID registered in that collection.
145
+ # yRest rewrites this file on first run, replacing all directives with real values.
142
146
  users:
143
- - id: 1
147
+ - id: __uuid_gen:ana
144
148
  name: Ana Garc\xEDa
145
149
  email: ana@example.com
146
150
  role: author
147
- - id: 2
151
+ - id: __uuid_gen:luis
148
152
  name: Luis Mart\xEDnez
149
153
  email: luis@example.com
150
154
  role: author
151
- - id: 3
155
+ - id: __uuid_gen:sara
152
156
  name: Sara L\xF3pez
153
157
  email: sara@example.com
154
158
  role: reader
155
159
 
156
160
  profiles:
157
- - id: 1
158
- userId: 1
161
+ - id: __uuid_gen
162
+ userId: __fk.users:ana
159
163
  bio: Full-stack developer and open-source enthusiast
160
164
  avatar: https://i.pravatar.cc/150?img=1
161
165
  website: https://ana.dev
162
- - id: 2
163
- userId: 2
166
+ - id: __uuid_gen
167
+ userId: __fk.users:luis
164
168
  bio: Backend engineer, coffee addict
165
169
  avatar: https://i.pravatar.cc/150?img=2
166
170
  website: https://luisdev.io
167
- - id: 3
168
- userId: 3
171
+ - id: __uuid_gen
172
+ userId: __fk.users:sara
169
173
  bio: Designer turned frontend developer
170
174
  avatar: https://i.pravatar.cc/150?img=3
171
175
  website: null
@@ -188,75 +192,75 @@ tags:
188
192
  color: "#ef4444"
189
193
 
190
194
  posts:
191
- - id: 1
195
+ - id: __uuid_gen:post-ts
192
196
  title: Getting started with TypeScript
193
197
  slug: getting-started-typescript
194
198
  body: TypeScript adds static typing to JavaScript, catching errors at compile time...
195
- userId: 1
199
+ userId: __fk.users:ana
196
200
  published: true
197
201
  views: 1420
198
202
  createdAt: "2024-11-01"
199
- - id: 2
203
+ - id: __uuid_gen:post-fastify
200
204
  title: Building REST APIs with Fastify
201
205
  slug: rest-apis-fastify
202
206
  body: Fastify is the fastest Node.js web framework, perfect for building APIs...
203
- userId: 1
207
+ userId: __fk.users:ana
204
208
  published: true
205
209
  views: 980
206
210
  createdAt: "2024-11-15"
207
- - id: 3
211
+ - id: __uuid_gen:post-testing
208
212
  title: Testing strategies for modern apps
209
213
  slug: testing-strategies-modern
210
214
  body: A solid test strategy covers unit, integration and end-to-end scenarios...
211
- userId: 2
215
+ userId: __fk.users:luis
212
216
  published: true
213
217
  views: 640
214
218
  createdAt: "2024-12-03"
215
- - id: 4
219
+ - id: __uuid_gen:post-yaml
216
220
  title: YAML as a database format
217
221
  slug: yaml-database-format
218
222
  body: YAML is human-readable and expressive enough for mock data during development...
219
- userId: 2
223
+ userId: __fk.users:luis
220
224
  published: false
221
225
  views: 0
222
226
  createdAt: "2025-01-10"
223
227
 
224
228
  # pivot table for posts \u2194 tags (many2many)
225
229
  post_tags:
226
- - { id: 1, postId: 1, tagId: 1 }
227
- - { id: 2, postId: 1, tagId: 2 }
228
- - { id: 3, postId: 2, tagId: 2 }
229
- - { id: 4, postId: 2, tagId: 4 }
230
- - { id: 5, postId: 3, tagId: 3 }
231
- - { id: 6, postId: 3, tagId: 1 }
232
- - { id: 7, postId: 4, tagId: 5 }
233
- - { id: 8, postId: 4, tagId: 2 }
230
+ - { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
231
+ - { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
232
+ - { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
233
+ - { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
234
+ - { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
235
+ - { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
236
+ - { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
237
+ - { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
234
238
 
235
239
  comments:
236
240
  - id: 1
237
241
  body: Great introduction! This helped me a lot.
238
- postId: 1
239
- userId: 3
242
+ postId: __fk.posts:post-ts
243
+ userId: __fk.users:sara
240
244
  likes: 5
241
245
  - id: 2
242
246
  body: Could you cover generics in a follow-up post?
243
- postId: 1
244
- userId: 2
247
+ postId: __fk.posts:post-ts
248
+ userId: __fk.users:luis
245
249
  likes: 3
246
250
  - id: 3
247
251
  body: Fastify is indeed much faster than Express in my benchmarks.
248
- postId: 2
249
- userId: 3
252
+ postId: __fk.posts:post-fastify
253
+ userId: __fk.users:sara
250
254
  likes: 8
251
255
  - id: 4
252
256
  body: Do you have a GitHub repo with these examples?
253
- postId: 2
254
- userId: 1
257
+ postId: __fk.posts:post-fastify
258
+ userId: __fk.users:ana
255
259
  likes: 1
256
260
  - id: 5
257
261
  body: E2E tests are underrated. Solid post!
258
- postId: 3
259
- userId: 1
262
+ postId: __fk.posts:post-testing
263
+ userId: __fk.users:ana
260
264
  likes: 4
261
265
 
262
266
  # Try these queries:
@@ -546,7 +550,7 @@ var import_node_path4 = require("path");
546
550
  // src/storage/yrestStorage.ts
547
551
  var import_node_fs2 = require("fs");
548
552
  var import_node_path2 = require("path");
549
- var import_node_crypto = require("crypto");
553
+ var import_node_crypto2 = require("crypto");
550
554
  var import_yaml = require("yaml");
551
555
 
552
556
  // src/storage/parseRelations.ts
@@ -818,6 +822,51 @@ function hasDataBlock(raw) {
818
822
  return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
819
823
  }
820
824
 
825
+ // src/storage/resolveDirectives.ts
826
+ var import_node_crypto = require("crypto");
827
+ var UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
828
+ var FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
829
+ function resolveDirectives(data) {
830
+ const registry = /* @__PURE__ */ new Map();
831
+ let modified = false;
832
+ for (const [collection, items] of Object.entries(data)) {
833
+ for (const item of items) {
834
+ for (const [field, value] of Object.entries(item)) {
835
+ if (typeof value !== "string") continue;
836
+ const m = UUID_GEN_RE.exec(value);
837
+ if (!m) continue;
838
+ const uuid = (0, import_node_crypto.randomUUID)();
839
+ item[field] = uuid;
840
+ modified = true;
841
+ const alias = m[1];
842
+ if (alias) {
843
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
844
+ registry.get(collection).set(alias, uuid);
845
+ }
846
+ }
847
+ }
848
+ }
849
+ for (const items of Object.values(data)) {
850
+ for (const item of items) {
851
+ for (const [field, value] of Object.entries(item)) {
852
+ if (typeof value !== "string") continue;
853
+ const m = FK_RE.exec(value);
854
+ if (!m) continue;
855
+ const col = m[1];
856
+ const alias = m[2];
857
+ const uuid = registry.get(col)?.get(alias);
858
+ if (uuid !== void 0) {
859
+ item[field] = uuid;
860
+ modified = true;
861
+ } else {
862
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
863
+ }
864
+ }
865
+ }
866
+ }
867
+ return modified;
868
+ }
869
+
821
870
  // src/utils/deepCopy.ts
822
871
  function deepCopyData(source) {
823
872
  return Object.fromEntries(
@@ -889,12 +938,13 @@ function createYrestStorage(filePath) {
889
938
  const schema = parseSchema(raw["_schema"]);
890
939
  const dataBlock = hasDataBlock(raw);
891
940
  const data = parseData(raw);
941
+ const directivesModified = resolveDirectives(data);
892
942
  let snapshot = {
893
943
  data: deepCopyData(data),
894
944
  relations: { ...relations },
895
945
  savedAt: /* @__PURE__ */ new Date()
896
946
  };
897
- return {
947
+ const storage = {
898
948
  getData() {
899
949
  return data;
900
950
  },
@@ -925,7 +975,7 @@ function createYrestStorage(filePath) {
925
975
  } else {
926
976
  Object.assign(payload, data);
927
977
  }
928
- const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto.randomUUID)()}.tmp`);
978
+ const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
929
979
  (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml.stringify)(payload), "utf8");
930
980
  (0, import_node_fs2.renameSync)(tmp, absPath);
931
981
  },
@@ -937,6 +987,7 @@ function createYrestStorage(filePath) {
937
987
  Object.assign(data, freshData);
938
988
  for (const key of Object.keys(relations)) delete relations[key];
939
989
  Object.assign(relations, freshRelations);
990
+ if (resolveDirectives(data)) storage.persist();
940
991
  },
941
992
  getSnapshot() {
942
993
  return snapshot;
@@ -957,6 +1008,8 @@ function createYrestStorage(filePath) {
957
1008
  this.persist();
958
1009
  }
959
1010
  };
1011
+ if (directivesModified) storage.persist();
1012
+ return storage;
960
1013
  }
961
1014
 
962
1015
  // src/server/createServer.ts
@@ -969,7 +1022,7 @@ var import_node_path3 = require("path");
969
1022
  var import_node_url = require("url");
970
1023
 
971
1024
  // src/utils/interpolate.ts
972
- var import_node_crypto2 = require("crypto");
1025
+ var import_node_crypto3 = require("crypto");
973
1026
  function getPath(obj, path) {
974
1027
  return path.split(".").reduce((acc, key) => {
975
1028
  if (acc != null && typeof acc === "object") return acc[key];
@@ -978,7 +1031,7 @@ function getPath(obj, path) {
978
1031
  }
979
1032
  function resolveVar(path, ctx) {
980
1033
  if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
981
- if (path === "uuid") return (0, import_node_crypto2.randomUUID)();
1034
+ if (path === "uuid") return (0, import_node_crypto3.randomUUID)();
982
1035
  if (path === "body") return ctx.body;
983
1036
  if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
984
1037
  if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
@@ -3483,11 +3536,18 @@ function registerServe(program2) {
3483
3536
  "--id-strategy <strategy>",
3484
3537
  "Id generation strategy for new items: increment (default) or uuid",
3485
3538
  "increment"
3539
+ ).option(
3540
+ "--uuid",
3541
+ "Shorthand for --id-strategy uuid: assign UUID ids to new items created via POST"
3486
3542
  ).action(async (file, flags, cmd) => {
3487
3543
  const fileConfig = loadConfigFile((0, import_node_path4.join)(process.cwd(), "yrest.config.yml"));
3488
3544
  const cliOverrides = Object.fromEntries(
3489
3545
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3490
3546
  );
3547
+ if (cliOverrides["uuid"] === true) {
3548
+ cliOverrides["idStrategy"] = "uuid";
3549
+ delete cliOverrides["uuid"];
3550
+ }
3491
3551
  const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3492
3552
  let storage;
3493
3553
  try {
@@ -88,6 +88,7 @@ categories:
88
88
  // src/cli/commands/templates/relational.ts
89
89
  var relationalTemplate = `# yrest relational sample \u2014 blog
90
90
  # Demonstrates: many2one, one2one and many2many relationships
91
+ # Also shows __uuid_gen and __fk directives for UUID-based IDs
91
92
  # Run: npx @yrest/cli serve db.yml
92
93
  # Docs: GET http://localhost:3070/_about
93
94
 
@@ -112,33 +113,36 @@ _rel:
112
113
  _type: one2one
113
114
  _target: users
114
115
 
116
+ # __uuid_gen:alias generates a UUID and registers it under that alias.
117
+ # __fk.collection:alias resolves to the UUID registered in that collection.
118
+ # yRest rewrites this file on first run, replacing all directives with real values.
115
119
  users:
116
- - id: 1
120
+ - id: __uuid_gen:ana
117
121
  name: Ana Garc\xEDa
118
122
  email: ana@example.com
119
123
  role: author
120
- - id: 2
124
+ - id: __uuid_gen:luis
121
125
  name: Luis Mart\xEDnez
122
126
  email: luis@example.com
123
127
  role: author
124
- - id: 3
128
+ - id: __uuid_gen:sara
125
129
  name: Sara L\xF3pez
126
130
  email: sara@example.com
127
131
  role: reader
128
132
 
129
133
  profiles:
130
- - id: 1
131
- userId: 1
134
+ - id: __uuid_gen
135
+ userId: __fk.users:ana
132
136
  bio: Full-stack developer and open-source enthusiast
133
137
  avatar: https://i.pravatar.cc/150?img=1
134
138
  website: https://ana.dev
135
- - id: 2
136
- userId: 2
139
+ - id: __uuid_gen
140
+ userId: __fk.users:luis
137
141
  bio: Backend engineer, coffee addict
138
142
  avatar: https://i.pravatar.cc/150?img=2
139
143
  website: https://luisdev.io
140
- - id: 3
141
- userId: 3
144
+ - id: __uuid_gen
145
+ userId: __fk.users:sara
142
146
  bio: Designer turned frontend developer
143
147
  avatar: https://i.pravatar.cc/150?img=3
144
148
  website: null
@@ -161,75 +165,75 @@ tags:
161
165
  color: "#ef4444"
162
166
 
163
167
  posts:
164
- - id: 1
168
+ - id: __uuid_gen:post-ts
165
169
  title: Getting started with TypeScript
166
170
  slug: getting-started-typescript
167
171
  body: TypeScript adds static typing to JavaScript, catching errors at compile time...
168
- userId: 1
172
+ userId: __fk.users:ana
169
173
  published: true
170
174
  views: 1420
171
175
  createdAt: "2024-11-01"
172
- - id: 2
176
+ - id: __uuid_gen:post-fastify
173
177
  title: Building REST APIs with Fastify
174
178
  slug: rest-apis-fastify
175
179
  body: Fastify is the fastest Node.js web framework, perfect for building APIs...
176
- userId: 1
180
+ userId: __fk.users:ana
177
181
  published: true
178
182
  views: 980
179
183
  createdAt: "2024-11-15"
180
- - id: 3
184
+ - id: __uuid_gen:post-testing
181
185
  title: Testing strategies for modern apps
182
186
  slug: testing-strategies-modern
183
187
  body: A solid test strategy covers unit, integration and end-to-end scenarios...
184
- userId: 2
188
+ userId: __fk.users:luis
185
189
  published: true
186
190
  views: 640
187
191
  createdAt: "2024-12-03"
188
- - id: 4
192
+ - id: __uuid_gen:post-yaml
189
193
  title: YAML as a database format
190
194
  slug: yaml-database-format
191
195
  body: YAML is human-readable and expressive enough for mock data during development...
192
- userId: 2
196
+ userId: __fk.users:luis
193
197
  published: false
194
198
  views: 0
195
199
  createdAt: "2025-01-10"
196
200
 
197
201
  # pivot table for posts \u2194 tags (many2many)
198
202
  post_tags:
199
- - { id: 1, postId: 1, tagId: 1 }
200
- - { id: 2, postId: 1, tagId: 2 }
201
- - { id: 3, postId: 2, tagId: 2 }
202
- - { id: 4, postId: 2, tagId: 4 }
203
- - { id: 5, postId: 3, tagId: 3 }
204
- - { id: 6, postId: 3, tagId: 1 }
205
- - { id: 7, postId: 4, tagId: 5 }
206
- - { id: 8, postId: 4, tagId: 2 }
203
+ - { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
204
+ - { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
205
+ - { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
206
+ - { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
207
+ - { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
208
+ - { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
209
+ - { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
210
+ - { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
207
211
 
208
212
  comments:
209
213
  - id: 1
210
214
  body: Great introduction! This helped me a lot.
211
- postId: 1
212
- userId: 3
215
+ postId: __fk.posts:post-ts
216
+ userId: __fk.users:sara
213
217
  likes: 5
214
218
  - id: 2
215
219
  body: Could you cover generics in a follow-up post?
216
- postId: 1
217
- userId: 2
220
+ postId: __fk.posts:post-ts
221
+ userId: __fk.users:luis
218
222
  likes: 3
219
223
  - id: 3
220
224
  body: Fastify is indeed much faster than Express in my benchmarks.
221
- postId: 2
222
- userId: 3
225
+ postId: __fk.posts:post-fastify
226
+ userId: __fk.users:sara
223
227
  likes: 8
224
228
  - id: 4
225
229
  body: Do you have a GitHub repo with these examples?
226
- postId: 2
227
- userId: 1
230
+ postId: __fk.posts:post-fastify
231
+ userId: __fk.users:ana
228
232
  likes: 1
229
233
  - id: 5
230
234
  body: E2E tests are underrated. Solid post!
231
- postId: 3
232
- userId: 1
235
+ postId: __fk.posts:post-testing
236
+ userId: __fk.users:ana
233
237
  likes: 4
234
238
 
235
239
  # Try these queries:
@@ -519,7 +523,7 @@ import { join as join2, resolve as resolve3 } from "path";
519
523
  // src/storage/yrestStorage.ts
520
524
  import { readFileSync, writeFileSync as writeFileSync2, renameSync } from "fs";
521
525
  import { resolve as resolve2, dirname } from "path";
522
- import { randomUUID } from "crypto";
526
+ import { randomUUID as randomUUID2 } from "crypto";
523
527
  import { parse, stringify } from "yaml";
524
528
 
525
529
  // src/storage/parseRelations.ts
@@ -791,6 +795,51 @@ function hasDataBlock(raw) {
791
795
  return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
792
796
  }
793
797
 
798
+ // src/storage/resolveDirectives.ts
799
+ import { randomUUID } from "crypto";
800
+ var UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
801
+ var FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
802
+ function resolveDirectives(data) {
803
+ const registry = /* @__PURE__ */ new Map();
804
+ let modified = false;
805
+ for (const [collection, items] of Object.entries(data)) {
806
+ for (const item of items) {
807
+ for (const [field, value] of Object.entries(item)) {
808
+ if (typeof value !== "string") continue;
809
+ const m = UUID_GEN_RE.exec(value);
810
+ if (!m) continue;
811
+ const uuid = randomUUID();
812
+ item[field] = uuid;
813
+ modified = true;
814
+ const alias = m[1];
815
+ if (alias) {
816
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
817
+ registry.get(collection).set(alias, uuid);
818
+ }
819
+ }
820
+ }
821
+ }
822
+ for (const items of Object.values(data)) {
823
+ for (const item of items) {
824
+ for (const [field, value] of Object.entries(item)) {
825
+ if (typeof value !== "string") continue;
826
+ const m = FK_RE.exec(value);
827
+ if (!m) continue;
828
+ const col = m[1];
829
+ const alias = m[2];
830
+ const uuid = registry.get(col)?.get(alias);
831
+ if (uuid !== void 0) {
832
+ item[field] = uuid;
833
+ modified = true;
834
+ } else {
835
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
836
+ }
837
+ }
838
+ }
839
+ }
840
+ return modified;
841
+ }
842
+
794
843
  // src/utils/deepCopy.ts
795
844
  function deepCopyData(source) {
796
845
  return Object.fromEntries(
@@ -862,12 +911,13 @@ function createYrestStorage(filePath) {
862
911
  const schema = parseSchema(raw["_schema"]);
863
912
  const dataBlock = hasDataBlock(raw);
864
913
  const data = parseData(raw);
914
+ const directivesModified = resolveDirectives(data);
865
915
  let snapshot = {
866
916
  data: deepCopyData(data),
867
917
  relations: { ...relations },
868
918
  savedAt: /* @__PURE__ */ new Date()
869
919
  };
870
- return {
920
+ const storage = {
871
921
  getData() {
872
922
  return data;
873
923
  },
@@ -898,7 +948,7 @@ function createYrestStorage(filePath) {
898
948
  } else {
899
949
  Object.assign(payload, data);
900
950
  }
901
- const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID()}.tmp`);
951
+ const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID2()}.tmp`);
902
952
  writeFileSync2(tmp, stringify(payload), "utf8");
903
953
  renameSync(tmp, absPath);
904
954
  },
@@ -910,6 +960,7 @@ function createYrestStorage(filePath) {
910
960
  Object.assign(data, freshData);
911
961
  for (const key of Object.keys(relations)) delete relations[key];
912
962
  Object.assign(relations, freshRelations);
963
+ if (resolveDirectives(data)) storage.persist();
913
964
  },
914
965
  getSnapshot() {
915
966
  return snapshot;
@@ -930,6 +981,8 @@ function createYrestStorage(filePath) {
930
981
  this.persist();
931
982
  }
932
983
  };
984
+ if (directivesModified) storage.persist();
985
+ return storage;
933
986
  }
934
987
 
935
988
  // src/server/createServer.ts
@@ -942,7 +995,7 @@ import { dirname as dirname2, join } from "path";
942
995
  import { fileURLToPath } from "url";
943
996
 
944
997
  // src/utils/interpolate.ts
945
- import { randomUUID as randomUUID2 } from "crypto";
998
+ import { randomUUID as randomUUID3 } from "crypto";
946
999
  function getPath(obj, path) {
947
1000
  return path.split(".").reduce((acc, key) => {
948
1001
  if (acc != null && typeof acc === "object") return acc[key];
@@ -951,7 +1004,7 @@ function getPath(obj, path) {
951
1004
  }
952
1005
  function resolveVar(path, ctx) {
953
1006
  if (path === "now") return (/* @__PURE__ */ new Date()).toISOString();
954
- if (path === "uuid") return randomUUID2();
1007
+ if (path === "uuid") return randomUUID3();
955
1008
  if (path === "body") return ctx.body;
956
1009
  if (path.startsWith("body.")) return getPath(ctx.body, path.slice(5));
957
1010
  if (path.startsWith("params.")) return ctx.params[path.slice(7)] ?? "";
@@ -3456,11 +3509,18 @@ function registerServe(program2) {
3456
3509
  "--id-strategy <strategy>",
3457
3510
  "Id generation strategy for new items: increment (default) or uuid",
3458
3511
  "increment"
3512
+ ).option(
3513
+ "--uuid",
3514
+ "Shorthand for --id-strategy uuid: assign UUID ids to new items created via POST"
3459
3515
  ).action(async (file, flags, cmd) => {
3460
3516
  const fileConfig = loadConfigFile(join2(process.cwd(), "yrest.config.yml"));
3461
3517
  const cliOverrides = Object.fromEntries(
3462
3518
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3463
3519
  );
3520
+ if (cliOverrides["uuid"] === true) {
3521
+ cliOverrides["idStrategy"] = "uuid";
3522
+ delete cliOverrides["uuid"];
3523
+ }
3464
3524
  const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3465
3525
  let storage;
3466
3526
  try {
package/dist/index.js CHANGED
@@ -355,6 +355,58 @@ var init_parseData = __esm({
355
355
  }
356
356
  });
357
357
 
358
+ // src/storage/resolveDirectives.ts
359
+ function resolveDirectives(data) {
360
+ const registry = /* @__PURE__ */ new Map();
361
+ let modified = false;
362
+ for (const [collection, items] of Object.entries(data)) {
363
+ for (const item of items) {
364
+ for (const [field, value] of Object.entries(item)) {
365
+ if (typeof value !== "string") continue;
366
+ const m = UUID_GEN_RE.exec(value);
367
+ if (!m) continue;
368
+ const uuid = (0, import_node_crypto2.randomUUID)();
369
+ item[field] = uuid;
370
+ modified = true;
371
+ const alias = m[1];
372
+ if (alias) {
373
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
374
+ registry.get(collection).set(alias, uuid);
375
+ }
376
+ }
377
+ }
378
+ }
379
+ for (const items of Object.values(data)) {
380
+ for (const item of items) {
381
+ for (const [field, value] of Object.entries(item)) {
382
+ if (typeof value !== "string") continue;
383
+ const m = FK_RE.exec(value);
384
+ if (!m) continue;
385
+ const col = m[1];
386
+ const alias = m[2];
387
+ const uuid = registry.get(col)?.get(alias);
388
+ if (uuid !== void 0) {
389
+ item[field] = uuid;
390
+ modified = true;
391
+ } else {
392
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ return modified;
398
+ }
399
+ var import_node_crypto2, UUID_GEN_RE, FK_RE;
400
+ var init_resolveDirectives = __esm({
401
+ "src/storage/resolveDirectives.ts"() {
402
+ "use strict";
403
+ init_cjs_shims();
404
+ import_node_crypto2 = require("crypto");
405
+ UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
406
+ FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
407
+ }
408
+ });
409
+
358
410
  // src/storage/yrestStorage.ts
359
411
  var yrestStorage_exports = {};
360
412
  __export(yrestStorage_exports, {
@@ -423,12 +475,13 @@ function createYrestStorage(filePath) {
423
475
  const schema = parseSchema(raw["_schema"]);
424
476
  const dataBlock = hasDataBlock(raw);
425
477
  const data = parseData(raw);
478
+ const directivesModified = resolveDirectives(data);
426
479
  let snapshot = {
427
480
  data: deepCopyData(data),
428
481
  relations: { ...relations },
429
482
  savedAt: /* @__PURE__ */ new Date()
430
483
  };
431
- return {
484
+ const storage = {
432
485
  getData() {
433
486
  return data;
434
487
  },
@@ -459,7 +512,7 @@ function createYrestStorage(filePath) {
459
512
  } else {
460
513
  Object.assign(payload, data);
461
514
  }
462
- const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
515
+ const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto3.randomUUID)()}.tmp`);
463
516
  (0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml3.stringify)(payload), "utf8");
464
517
  (0, import_node_fs3.renameSync)(tmp, absPath);
465
518
  },
@@ -471,6 +524,7 @@ function createYrestStorage(filePath) {
471
524
  Object.assign(data, freshData);
472
525
  for (const key of Object.keys(relations)) delete relations[key];
473
526
  Object.assign(relations, freshRelations);
527
+ if (resolveDirectives(data)) storage.persist();
474
528
  },
475
529
  getSnapshot() {
476
530
  return snapshot;
@@ -491,21 +545,24 @@ function createYrestStorage(filePath) {
491
545
  this.persist();
492
546
  }
493
547
  };
548
+ if (directivesModified) storage.persist();
549
+ return storage;
494
550
  }
495
- var import_node_fs3, import_node_path2, import_node_crypto2, import_yaml3;
551
+ var import_node_fs3, import_node_path2, import_node_crypto3, import_yaml3;
496
552
  var init_yrestStorage = __esm({
497
553
  "src/storage/yrestStorage.ts"() {
498
554
  "use strict";
499
555
  init_cjs_shims();
500
556
  import_node_fs3 = require("fs");
501
557
  import_node_path2 = require("path");
502
- import_node_crypto2 = require("crypto");
558
+ import_node_crypto3 = require("crypto");
503
559
  import_yaml3 = require("yaml");
504
560
  init_parseRelations();
505
561
  init_parseRoutes();
506
562
  init_parseSseRoutes();
507
563
  init_parseSchema();
508
564
  init_parseData();
565
+ init_resolveDirectives();
509
566
  init_deepCopy();
510
567
  }
511
568
  });
package/dist/index.mjs CHANGED
@@ -332,6 +332,58 @@ var init_parseData = __esm({
332
332
  }
333
333
  });
334
334
 
335
+ // src/storage/resolveDirectives.ts
336
+ import { randomUUID as randomUUID2 } from "crypto";
337
+ function resolveDirectives(data) {
338
+ const registry = /* @__PURE__ */ new Map();
339
+ let modified = false;
340
+ for (const [collection, items] of Object.entries(data)) {
341
+ for (const item of items) {
342
+ for (const [field, value] of Object.entries(item)) {
343
+ if (typeof value !== "string") continue;
344
+ const m = UUID_GEN_RE.exec(value);
345
+ if (!m) continue;
346
+ const uuid = randomUUID2();
347
+ item[field] = uuid;
348
+ modified = true;
349
+ const alias = m[1];
350
+ if (alias) {
351
+ if (!registry.has(collection)) registry.set(collection, /* @__PURE__ */ new Map());
352
+ registry.get(collection).set(alias, uuid);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ for (const items of Object.values(data)) {
358
+ for (const item of items) {
359
+ for (const [field, value] of Object.entries(item)) {
360
+ if (typeof value !== "string") continue;
361
+ const m = FK_RE.exec(value);
362
+ if (!m) continue;
363
+ const col = m[1];
364
+ const alias = m[2];
365
+ const uuid = registry.get(col)?.get(alias);
366
+ if (uuid !== void 0) {
367
+ item[field] = uuid;
368
+ modified = true;
369
+ } else {
370
+ console.warn(`yrest: cannot resolve __fk.${col}:${alias} \u2014 alias not registered`);
371
+ }
372
+ }
373
+ }
374
+ }
375
+ return modified;
376
+ }
377
+ var UUID_GEN_RE, FK_RE;
378
+ var init_resolveDirectives = __esm({
379
+ "src/storage/resolveDirectives.ts"() {
380
+ "use strict";
381
+ init_esm_shims();
382
+ UUID_GEN_RE = /^__uuid_gen(?::([a-zA-Z0-9_-]+))?$/;
383
+ FK_RE = /^__fk\.([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/;
384
+ }
385
+ });
386
+
335
387
  // src/storage/yrestStorage.ts
336
388
  var yrestStorage_exports = {};
337
389
  __export(yrestStorage_exports, {
@@ -339,7 +391,7 @@ __export(yrestStorage_exports, {
339
391
  });
340
392
  import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
341
393
  import { resolve, dirname as dirname2 } from "path";
342
- import { randomUUID as randomUUID2 } from "crypto";
394
+ import { randomUUID as randomUUID3 } from "crypto";
343
395
  import { parse as parse2, stringify as stringify2 } from "yaml";
344
396
  function serializeRelations(relations) {
345
397
  const out = {};
@@ -404,12 +456,13 @@ function createYrestStorage(filePath) {
404
456
  const schema = parseSchema(raw["_schema"]);
405
457
  const dataBlock = hasDataBlock(raw);
406
458
  const data = parseData(raw);
459
+ const directivesModified = resolveDirectives(data);
407
460
  let snapshot = {
408
461
  data: deepCopyData(data),
409
462
  relations: { ...relations },
410
463
  savedAt: /* @__PURE__ */ new Date()
411
464
  };
412
- return {
465
+ const storage = {
413
466
  getData() {
414
467
  return data;
415
468
  },
@@ -440,7 +493,7 @@ function createYrestStorage(filePath) {
440
493
  } else {
441
494
  Object.assign(payload, data);
442
495
  }
443
- const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
496
+ const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID3()}.tmp`);
444
497
  writeFileSync(tmp, stringify2(payload), "utf8");
445
498
  renameSync(tmp, absPath);
446
499
  },
@@ -452,6 +505,7 @@ function createYrestStorage(filePath) {
452
505
  Object.assign(data, freshData);
453
506
  for (const key of Object.keys(relations)) delete relations[key];
454
507
  Object.assign(relations, freshRelations);
508
+ if (resolveDirectives(data)) storage.persist();
455
509
  },
456
510
  getSnapshot() {
457
511
  return snapshot;
@@ -472,6 +526,8 @@ function createYrestStorage(filePath) {
472
526
  this.persist();
473
527
  }
474
528
  };
529
+ if (directivesModified) storage.persist();
530
+ return storage;
475
531
  }
476
532
  var init_yrestStorage = __esm({
477
533
  "src/storage/yrestStorage.ts"() {
@@ -482,6 +538,7 @@ var init_yrestStorage = __esm({
482
538
  init_parseSseRoutes();
483
539
  init_parseSchema();
484
540
  init_parseData();
541
+ init_resolveDirectives();
485
542
  init_deepCopy();
486
543
  }
487
544
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
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",