@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 +26 -1
- package/dist/cli/index.js +100 -40
- package/dist/cli/index.mjs +100 -40
- package/dist/index.js +61 -4
- package/dist/index.mjs +60 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://github.com/aggiovato/yRest/actions)
|
|
11
11
|
[](https://www.npmjs.com/package/@yrest/cli)
|
|
12
12
|
[](https://www.typescriptlang.org/)
|
|
13
|
-
[](https://socket.dev/npm/package/@yrest/cli)
|
|
14
14
|
|
|
15
15
|
YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
|
|
16
16
|
|
|
@@ -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:
|
|
147
|
+
- id: __uuid_gen:ana
|
|
144
148
|
name: Ana Garc\xEDa
|
|
145
149
|
email: ana@example.com
|
|
146
150
|
role: author
|
|
147
|
-
- id:
|
|
151
|
+
- id: __uuid_gen:luis
|
|
148
152
|
name: Luis Mart\xEDnez
|
|
149
153
|
email: luis@example.com
|
|
150
154
|
role: author
|
|
151
|
-
- id:
|
|
155
|
+
- id: __uuid_gen:sara
|
|
152
156
|
name: Sara L\xF3pez
|
|
153
157
|
email: sara@example.com
|
|
154
158
|
role: reader
|
|
155
159
|
|
|
156
160
|
profiles:
|
|
157
|
-
- id:
|
|
158
|
-
userId:
|
|
161
|
+
- id: __uuid_gen
|
|
162
|
+
userId: __fk.users:ana
|
|
159
163
|
bio: Full-stack developer and open-source enthusiast
|
|
160
164
|
avatar: https://i.pravatar.cc/150?img=1
|
|
161
165
|
website: https://ana.dev
|
|
162
|
-
- id:
|
|
163
|
-
userId:
|
|
166
|
+
- id: __uuid_gen
|
|
167
|
+
userId: __fk.users:luis
|
|
164
168
|
bio: Backend engineer, coffee addict
|
|
165
169
|
avatar: https://i.pravatar.cc/150?img=2
|
|
166
170
|
website: https://luisdev.io
|
|
167
|
-
- id:
|
|
168
|
-
userId:
|
|
171
|
+
- id: __uuid_gen
|
|
172
|
+
userId: __fk.users:sara
|
|
169
173
|
bio: Designer turned frontend developer
|
|
170
174
|
avatar: https://i.pravatar.cc/150?img=3
|
|
171
175
|
website: null
|
|
@@ -188,75 +192,75 @@ tags:
|
|
|
188
192
|
color: "#ef4444"
|
|
189
193
|
|
|
190
194
|
posts:
|
|
191
|
-
- id:
|
|
195
|
+
- id: __uuid_gen:post-ts
|
|
192
196
|
title: Getting started with TypeScript
|
|
193
197
|
slug: getting-started-typescript
|
|
194
198
|
body: TypeScript adds static typing to JavaScript, catching errors at compile time...
|
|
195
|
-
userId:
|
|
199
|
+
userId: __fk.users:ana
|
|
196
200
|
published: true
|
|
197
201
|
views: 1420
|
|
198
202
|
createdAt: "2024-11-01"
|
|
199
|
-
- id:
|
|
203
|
+
- id: __uuid_gen:post-fastify
|
|
200
204
|
title: Building REST APIs with Fastify
|
|
201
205
|
slug: rest-apis-fastify
|
|
202
206
|
body: Fastify is the fastest Node.js web framework, perfect for building APIs...
|
|
203
|
-
userId:
|
|
207
|
+
userId: __fk.users:ana
|
|
204
208
|
published: true
|
|
205
209
|
views: 980
|
|
206
210
|
createdAt: "2024-11-15"
|
|
207
|
-
- id:
|
|
211
|
+
- id: __uuid_gen:post-testing
|
|
208
212
|
title: Testing strategies for modern apps
|
|
209
213
|
slug: testing-strategies-modern
|
|
210
214
|
body: A solid test strategy covers unit, integration and end-to-end scenarios...
|
|
211
|
-
userId:
|
|
215
|
+
userId: __fk.users:luis
|
|
212
216
|
published: true
|
|
213
217
|
views: 640
|
|
214
218
|
createdAt: "2024-12-03"
|
|
215
|
-
- id:
|
|
219
|
+
- id: __uuid_gen:post-yaml
|
|
216
220
|
title: YAML as a database format
|
|
217
221
|
slug: yaml-database-format
|
|
218
222
|
body: YAML is human-readable and expressive enough for mock data during development...
|
|
219
|
-
userId:
|
|
223
|
+
userId: __fk.users:luis
|
|
220
224
|
published: false
|
|
221
225
|
views: 0
|
|
222
226
|
createdAt: "2025-01-10"
|
|
223
227
|
|
|
224
228
|
# pivot table for posts \u2194 tags (many2many)
|
|
225
229
|
post_tags:
|
|
226
|
-
- { id: 1, postId:
|
|
227
|
-
- { id: 2, postId:
|
|
228
|
-
- { id: 3, postId:
|
|
229
|
-
- { id: 4, postId:
|
|
230
|
-
- { id: 5, postId:
|
|
231
|
-
- { id: 6, postId:
|
|
232
|
-
- { id: 7, postId:
|
|
233
|
-
- { id: 8, postId:
|
|
230
|
+
- { id: 1, postId: __fk.posts:post-ts, tagId: 1 }
|
|
231
|
+
- { id: 2, postId: __fk.posts:post-ts, tagId: 2 }
|
|
232
|
+
- { id: 3, postId: __fk.posts:post-fastify, tagId: 2 }
|
|
233
|
+
- { id: 4, postId: __fk.posts:post-fastify, tagId: 4 }
|
|
234
|
+
- { id: 5, postId: __fk.posts:post-testing, tagId: 3 }
|
|
235
|
+
- { id: 6, postId: __fk.posts:post-testing, tagId: 1 }
|
|
236
|
+
- { id: 7, postId: __fk.posts:post-yaml, tagId: 5 }
|
|
237
|
+
- { id: 8, postId: __fk.posts:post-yaml, tagId: 2 }
|
|
234
238
|
|
|
235
239
|
comments:
|
|
236
240
|
- id: 1
|
|
237
241
|
body: Great introduction! This helped me a lot.
|
|
238
|
-
postId:
|
|
239
|
-
userId:
|
|
242
|
+
postId: __fk.posts:post-ts
|
|
243
|
+
userId: __fk.users:sara
|
|
240
244
|
likes: 5
|
|
241
245
|
- id: 2
|
|
242
246
|
body: Could you cover generics in a follow-up post?
|
|
243
|
-
postId:
|
|
244
|
-
userId:
|
|
247
|
+
postId: __fk.posts:post-ts
|
|
248
|
+
userId: __fk.users:luis
|
|
245
249
|
likes: 3
|
|
246
250
|
- id: 3
|
|
247
251
|
body: Fastify is indeed much faster than Express in my benchmarks.
|
|
248
|
-
postId:
|
|
249
|
-
userId:
|
|
252
|
+
postId: __fk.posts:post-fastify
|
|
253
|
+
userId: __fk.users:sara
|
|
250
254
|
likes: 8
|
|
251
255
|
- id: 4
|
|
252
256
|
body: Do you have a GitHub repo with these examples?
|
|
253
|
-
postId:
|
|
254
|
-
userId:
|
|
257
|
+
postId: __fk.posts:post-fastify
|
|
258
|
+
userId: __fk.users:ana
|
|
255
259
|
likes: 1
|
|
256
260
|
- id: 5
|
|
257
261
|
body: E2E tests are underrated. Solid post!
|
|
258
|
-
postId:
|
|
259
|
-
userId:
|
|
262
|
+
postId: __fk.posts:post-testing
|
|
263
|
+
userId: __fk.users:ana
|
|
260
264
|
likes: 4
|
|
261
265
|
|
|
262
266
|
# Try these queries:
|
|
@@ -546,7 +550,7 @@ var import_node_path4 = require("path");
|
|
|
546
550
|
// src/storage/yrestStorage.ts
|
|
547
551
|
var import_node_fs2 = require("fs");
|
|
548
552
|
var import_node_path2 = require("path");
|
|
549
|
-
var
|
|
553
|
+
var import_node_crypto2 = require("crypto");
|
|
550
554
|
var import_yaml = require("yaml");
|
|
551
555
|
|
|
552
556
|
// src/storage/parseRelations.ts
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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 {
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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:
|
|
120
|
+
- id: __uuid_gen:ana
|
|
117
121
|
name: Ana Garc\xEDa
|
|
118
122
|
email: ana@example.com
|
|
119
123
|
role: author
|
|
120
|
-
- id:
|
|
124
|
+
- id: __uuid_gen:luis
|
|
121
125
|
name: Luis Mart\xEDnez
|
|
122
126
|
email: luis@example.com
|
|
123
127
|
role: author
|
|
124
|
-
- id:
|
|
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:
|
|
131
|
-
userId:
|
|
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:
|
|
136
|
-
userId:
|
|
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:
|
|
141
|
-
userId:
|
|
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:
|
|
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:
|
|
172
|
+
userId: __fk.users:ana
|
|
169
173
|
published: true
|
|
170
174
|
views: 1420
|
|
171
175
|
createdAt: "2024-11-01"
|
|
172
|
-
- id:
|
|
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:
|
|
180
|
+
userId: __fk.users:ana
|
|
177
181
|
published: true
|
|
178
182
|
views: 980
|
|
179
183
|
createdAt: "2024-11-15"
|
|
180
|
-
- id:
|
|
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:
|
|
188
|
+
userId: __fk.users:luis
|
|
185
189
|
published: true
|
|
186
190
|
views: 640
|
|
187
191
|
createdAt: "2024-12-03"
|
|
188
|
-
- id:
|
|
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:
|
|
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:
|
|
200
|
-
- { id: 2, postId:
|
|
201
|
-
- { id: 3, postId:
|
|
202
|
-
- { id: 4, postId:
|
|
203
|
-
- { id: 5, postId:
|
|
204
|
-
- { id: 6, postId:
|
|
205
|
-
- { id: 7, postId:
|
|
206
|
-
- { id: 8, postId:
|
|
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:
|
|
212
|
-
userId:
|
|
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:
|
|
217
|
-
userId:
|
|
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:
|
|
222
|
-
userId:
|
|
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:
|
|
227
|
-
userId:
|
|
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:
|
|
232
|
-
userId:
|
|
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
|
-
|
|
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-${
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-${
|
|
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