@yrest/cli 0.9.0 → 0.11.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 +158 -53
- package/assets/yrest-banner.png +0 -0
- package/assets/yrest-lockup.png +0 -0
- package/assets/yrest-lockup.svg +20 -0
- package/assets/yrest-logo.png +0 -0
- package/assets/yrest-monochrome.png +0 -0
- package/dist/cli/index.js +1273 -145
- package/dist/cli/index.mjs +1270 -142
- package/dist/index.d.mts +52 -10
- package/dist/index.d.ts +52 -10
- package/dist/index.js +1223 -94
- package/dist/index.mjs +1220 -91
- package/package.json +1 -1
- package/assets/logo-color.png +0 -0
- package/assets/logo-figure.png +0 -0
- package/assets/logo-text.png +0 -0
- package/assets/logo-white.png +0 -0
- package/assets/yRest-banner.png +0 -0
package/dist/index.js
CHANGED
|
@@ -56,37 +56,136 @@ function parseRelations(raw) {
|
|
|
56
56
|
}
|
|
57
57
|
function normaliseRelationDef(key, value) {
|
|
58
58
|
if (typeof value === "string") {
|
|
59
|
-
return { type: "many2one", target: value };
|
|
59
|
+
return value.includes(":") ? parseDslString(value) : { type: "many2one", target: value };
|
|
60
60
|
}
|
|
61
61
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
62
62
|
const v = value;
|
|
63
|
-
const type = v["
|
|
64
|
-
const nested = v["
|
|
63
|
+
const type = v["_type"];
|
|
64
|
+
const nested = v["_nested"] === true ? true : void 0;
|
|
65
|
+
const carDirect = typeof v["_car-direct"] === "string" ? v["_car-direct"] : void 0;
|
|
66
|
+
const carInverse = typeof v["_car-inverse"] === "string" ? v["_car-inverse"] : void 0;
|
|
65
67
|
if (type === "many2one" || type === void 0) {
|
|
66
|
-
const target = v["
|
|
68
|
+
const target = v["_target"];
|
|
67
69
|
if (typeof target !== "string") return null;
|
|
68
|
-
|
|
70
|
+
const foreignKey = typeof v["_foreignKey"] === "string" ? v["_foreignKey"] : void 0;
|
|
71
|
+
return compact({ type: "many2one", target, foreignKey, nested, carDirect, carInverse });
|
|
69
72
|
}
|
|
70
73
|
if (type === "one2one") {
|
|
71
|
-
const target = v["
|
|
74
|
+
const target = v["_target"];
|
|
72
75
|
if (typeof target !== "string") return null;
|
|
73
|
-
|
|
76
|
+
const foreignKey = typeof v["_foreignKey"] === "string" ? v["_foreignKey"] : void 0;
|
|
77
|
+
return compact({ type: "one2one", target, foreignKey, nested, carDirect, carInverse });
|
|
74
78
|
}
|
|
75
79
|
if (type === "many2many") {
|
|
76
|
-
const target = typeof v["
|
|
77
|
-
const through = v["
|
|
78
|
-
const foreignKey = v["
|
|
79
|
-
const otherKey = v["
|
|
80
|
+
const target = typeof v["_target"] === "string" ? v["_target"] : key;
|
|
81
|
+
const through = v["_through"];
|
|
82
|
+
const foreignKey = v["_foreignKey"];
|
|
83
|
+
const otherKey = v["_otherKey"];
|
|
80
84
|
if (typeof through !== "string" || typeof foreignKey !== "string" || typeof otherKey !== "string")
|
|
81
85
|
return null;
|
|
82
|
-
return
|
|
86
|
+
return compact({
|
|
87
|
+
type: "many2many",
|
|
88
|
+
target,
|
|
89
|
+
through,
|
|
90
|
+
foreignKey,
|
|
91
|
+
otherKey,
|
|
92
|
+
nested,
|
|
93
|
+
carDirect,
|
|
94
|
+
carInverse
|
|
95
|
+
});
|
|
83
96
|
}
|
|
84
97
|
return null;
|
|
85
98
|
}
|
|
99
|
+
function parseDslString(raw) {
|
|
100
|
+
const nested = raw.endsWith("+nested") ? true : void 0;
|
|
101
|
+
let s = nested ? raw.slice(0, -7) : raw;
|
|
102
|
+
let carDirect;
|
|
103
|
+
let carInverse;
|
|
104
|
+
const carMatch = s.match(/\[([^[\]]+)->([^[\]]+)\]$/);
|
|
105
|
+
if (carMatch) {
|
|
106
|
+
carDirect = carMatch[1];
|
|
107
|
+
carInverse = carMatch[2];
|
|
108
|
+
s = s.slice(0, s.length - carMatch[0].length);
|
|
109
|
+
}
|
|
110
|
+
const colonIdx = s.indexOf(":");
|
|
111
|
+
if (colonIdx === -1) return null;
|
|
112
|
+
const typeAlias = s.slice(0, colonIdx);
|
|
113
|
+
const body = s.slice(colonIdx + 1);
|
|
114
|
+
const type = TYPE_ALIASES[typeAlias];
|
|
115
|
+
if (!type) return null;
|
|
116
|
+
if (type === "many2many") {
|
|
117
|
+
const m = body.match(/^([^@()[\]]+)@([^@()[\]]+)\(([^,)]+),\s*([^,)]+)\)$/);
|
|
118
|
+
if (!m) return null;
|
|
119
|
+
const [, target2, through, foreignKey2, otherKey] = m;
|
|
120
|
+
return compact({
|
|
121
|
+
type: "many2many",
|
|
122
|
+
target: target2,
|
|
123
|
+
through,
|
|
124
|
+
foreignKey: foreignKey2,
|
|
125
|
+
otherKey,
|
|
126
|
+
nested,
|
|
127
|
+
carDirect,
|
|
128
|
+
carInverse
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const atIdx = body.indexOf("@");
|
|
132
|
+
const target = atIdx === -1 ? body : body.slice(0, atIdx);
|
|
133
|
+
const foreignKey = atIdx === -1 ? void 0 : body.slice(atIdx + 1);
|
|
134
|
+
if (!target) return null;
|
|
135
|
+
return compact({ type, target, foreignKey, nested, carDirect, carInverse });
|
|
136
|
+
}
|
|
137
|
+
function compact(obj) {
|
|
138
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
139
|
+
}
|
|
140
|
+
var TYPE_ALIASES;
|
|
86
141
|
var init_parseRelations = __esm({
|
|
87
142
|
"src/storage/parseRelations.ts"() {
|
|
88
143
|
"use strict";
|
|
89
144
|
init_cjs_shims();
|
|
145
|
+
TYPE_ALIASES = {
|
|
146
|
+
many2one: "many2one",
|
|
147
|
+
m2o: "many2one",
|
|
148
|
+
one2one: "one2one",
|
|
149
|
+
o2o: "one2one",
|
|
150
|
+
many2many: "many2many",
|
|
151
|
+
m2m: "many2many"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// src/storage/parseSchema.ts
|
|
157
|
+
function parseSchema(raw) {
|
|
158
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
159
|
+
const result = {};
|
|
160
|
+
for (const [collection, fields] of Object.entries(raw)) {
|
|
161
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) continue;
|
|
162
|
+
result[collection] = {};
|
|
163
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
164
|
+
const def = normaliseFieldDef(value);
|
|
165
|
+
if (def) result[collection][field] = def;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function normaliseFieldDef(value) {
|
|
171
|
+
if (value === "required") return { required: true };
|
|
172
|
+
if (value === "optional") return { required: false };
|
|
173
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
174
|
+
const v = value;
|
|
175
|
+
const def = {};
|
|
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"]))
|
|
178
|
+
def.type = v["_type"];
|
|
179
|
+
if (typeof v["_format"] === "string") def.format = v["_format"];
|
|
180
|
+
if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
|
|
181
|
+
if (typeof v["_description"] === "string") def.description = v["_description"];
|
|
182
|
+
if (v["_default"] !== void 0) def.default = v["_default"];
|
|
183
|
+
return def;
|
|
184
|
+
}
|
|
185
|
+
var init_parseSchema = __esm({
|
|
186
|
+
"src/storage/parseSchema.ts"() {
|
|
187
|
+
"use strict";
|
|
188
|
+
init_cjs_shims();
|
|
90
189
|
}
|
|
91
190
|
});
|
|
92
191
|
|
|
@@ -103,18 +202,128 @@ var init_deepCopy = __esm({
|
|
|
103
202
|
}
|
|
104
203
|
});
|
|
105
204
|
|
|
205
|
+
// src/storage/parseRoutes.ts
|
|
206
|
+
function parseRoutes(raw) {
|
|
207
|
+
if (!Array.isArray(raw)) return [];
|
|
208
|
+
return raw.map(normaliseRoute).filter((r) => r !== null);
|
|
209
|
+
}
|
|
210
|
+
function normaliseRoute(raw) {
|
|
211
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
212
|
+
const v = raw;
|
|
213
|
+
const method = v["_method"];
|
|
214
|
+
const path = v["_path"];
|
|
215
|
+
if (typeof method !== "string" || typeof path !== "string") return null;
|
|
216
|
+
const route = { method, path };
|
|
217
|
+
if (typeof v["_handler"] === "string") route.handler = v["_handler"];
|
|
218
|
+
if (typeof v["_delay"] === "number") route.delay = v["_delay"];
|
|
219
|
+
if (typeof v["_error"] === "number") route.error = v["_error"];
|
|
220
|
+
if (v["_errorBody"] !== void 0) route.errorBody = v["_errorBody"];
|
|
221
|
+
const response = normaliseResponse(v["_response"]);
|
|
222
|
+
if (response) route.response = response;
|
|
223
|
+
const otherwise = normaliseResponse(v["_otherwise"]);
|
|
224
|
+
if (otherwise) route.otherwise = otherwise;
|
|
225
|
+
const scenarios = normaliseScenarios(v["_scenarios"]);
|
|
226
|
+
if (scenarios.length > 0) route.scenarios = scenarios;
|
|
227
|
+
return route;
|
|
228
|
+
}
|
|
229
|
+
function normaliseResponse(raw) {
|
|
230
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
231
|
+
const v = raw;
|
|
232
|
+
const response = {};
|
|
233
|
+
if (typeof v["_status"] === "number") response.status = v["_status"];
|
|
234
|
+
if (v["_body"] !== void 0) response.body = v["_body"];
|
|
235
|
+
if (v["_headers"] && typeof v["_headers"] === "object" && !Array.isArray(v["_headers"]))
|
|
236
|
+
response.headers = v["_headers"];
|
|
237
|
+
return Object.keys(response).length > 0 ? response : void 0;
|
|
238
|
+
}
|
|
239
|
+
function normaliseScenarios(raw) {
|
|
240
|
+
if (!Array.isArray(raw)) return [];
|
|
241
|
+
return raw.map(normaliseScenario).filter((s) => s !== null);
|
|
242
|
+
}
|
|
243
|
+
function normaliseScenario(raw) {
|
|
244
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
245
|
+
const v = raw;
|
|
246
|
+
const when = v["_when"];
|
|
247
|
+
if (!when || typeof when !== "object") return null;
|
|
248
|
+
const response = normaliseResponse(v["_response"]);
|
|
249
|
+
if (!response) return null;
|
|
250
|
+
return { when, response };
|
|
251
|
+
}
|
|
252
|
+
var init_parseRoutes = __esm({
|
|
253
|
+
"src/storage/parseRoutes.ts"() {
|
|
254
|
+
"use strict";
|
|
255
|
+
init_cjs_shims();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
106
259
|
// src/storage/yrestStorage.ts
|
|
107
260
|
var yrestStorage_exports = {};
|
|
108
261
|
__export(yrestStorage_exports, {
|
|
109
262
|
createYrestStorage: () => createYrestStorage
|
|
110
263
|
});
|
|
264
|
+
function serializeRelations(relations) {
|
|
265
|
+
const out = {};
|
|
266
|
+
for (const [col, fields] of Object.entries(relations)) {
|
|
267
|
+
out[col] = {};
|
|
268
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
269
|
+
out[col][key] = serializeRelationDef(def);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function serializeRelationDef(def) {
|
|
275
|
+
if (def.type === "many2many") {
|
|
276
|
+
const entry2 = {
|
|
277
|
+
_type: def.type,
|
|
278
|
+
_target: def.target,
|
|
279
|
+
_through: def.through,
|
|
280
|
+
_foreignKey: def.foreignKey,
|
|
281
|
+
_otherKey: def.otherKey
|
|
282
|
+
};
|
|
283
|
+
if (def.nested) entry2._nested = true;
|
|
284
|
+
if (def.carDirect) entry2["_car-direct"] = def.carDirect;
|
|
285
|
+
if (def.carInverse) entry2["_car-inverse"] = def.carInverse;
|
|
286
|
+
return entry2;
|
|
287
|
+
}
|
|
288
|
+
const entry = { _type: def.type, _target: def.target };
|
|
289
|
+
if (def.foreignKey) entry._foreignKey = def.foreignKey;
|
|
290
|
+
if (def.nested) entry._nested = true;
|
|
291
|
+
if (def.carDirect) entry["_car-direct"] = def.carDirect;
|
|
292
|
+
if (def.carInverse) entry["_car-inverse"] = def.carInverse;
|
|
293
|
+
return entry;
|
|
294
|
+
}
|
|
295
|
+
function serializeRoutes(routes) {
|
|
296
|
+
return routes.map((r) => {
|
|
297
|
+
const entry = { _method: r.method, _path: r.path };
|
|
298
|
+
if (r.handler) entry._handler = r.handler;
|
|
299
|
+
if (r.delay !== void 0) entry._delay = r.delay;
|
|
300
|
+
if (r.error !== void 0) entry._error = r.error;
|
|
301
|
+
if (r.errorBody !== void 0) entry._errorBody = r.errorBody;
|
|
302
|
+
if (r.response) entry._response = serializeResponse(r.response);
|
|
303
|
+
if (r.otherwise) entry._otherwise = serializeResponse(r.otherwise);
|
|
304
|
+
if (r.scenarios?.length) entry._scenarios = r.scenarios.map(serializeScenario);
|
|
305
|
+
return entry;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function serializeResponse(res) {
|
|
309
|
+
const out = {};
|
|
310
|
+
if (res.status !== void 0) out._status = res.status;
|
|
311
|
+
if (res.body !== void 0) out._body = res.body;
|
|
312
|
+
if (res.headers) out._headers = res.headers;
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
function serializeScenario(s) {
|
|
316
|
+
return { _when: s.when, _response: serializeResponse(s.response) };
|
|
317
|
+
}
|
|
111
318
|
function createYrestStorage(filePath) {
|
|
112
319
|
const absPath = (0, import_node_path2.resolve)(filePath);
|
|
113
|
-
const raw = (0,
|
|
320
|
+
const raw = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
321
|
+
const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
|
|
114
322
|
const relations = parseRelations(raw["_rel"]);
|
|
115
|
-
const routes =
|
|
323
|
+
const routes = parseRoutes(raw["_routes"]);
|
|
324
|
+
const schema = parseSchema(raw["_schema"]);
|
|
116
325
|
const data = Object.fromEntries(
|
|
117
|
-
Object.entries(raw).filter(([key]) => key
|
|
326
|
+
Object.entries(raw).filter(([key]) => !RESERVED.has(key))
|
|
118
327
|
);
|
|
119
328
|
let snapshot = {
|
|
120
329
|
data: deepCopyData(data),
|
|
@@ -128,6 +337,9 @@ function createYrestStorage(filePath) {
|
|
|
128
337
|
getRelations() {
|
|
129
338
|
return relations;
|
|
130
339
|
},
|
|
340
|
+
getSchema() {
|
|
341
|
+
return schema;
|
|
342
|
+
},
|
|
131
343
|
getRoutes() {
|
|
132
344
|
return routes;
|
|
133
345
|
},
|
|
@@ -139,18 +351,18 @@ function createYrestStorage(filePath) {
|
|
|
139
351
|
},
|
|
140
352
|
persist() {
|
|
141
353
|
const payload = {};
|
|
142
|
-
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
143
|
-
if (routes.length > 0) payload._routes = routes;
|
|
354
|
+
if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
|
|
355
|
+
if (routes.length > 0) payload._routes = serializeRoutes(routes);
|
|
144
356
|
Object.assign(payload, data);
|
|
145
357
|
const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
|
|
146
|
-
(0, import_node_fs3.writeFileSync)(tmp, (0,
|
|
358
|
+
(0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml3.stringify)(payload), "utf8");
|
|
147
359
|
(0, import_node_fs3.renameSync)(tmp, absPath);
|
|
148
360
|
},
|
|
149
361
|
reload() {
|
|
150
|
-
const fresh = (0,
|
|
362
|
+
const fresh = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
151
363
|
const freshRelations = parseRelations(fresh["_rel"]);
|
|
152
364
|
const freshData = Object.fromEntries(
|
|
153
|
-
Object.entries(fresh).filter(([key]) => key
|
|
365
|
+
Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
|
|
154
366
|
);
|
|
155
367
|
for (const key of Object.keys(data)) delete data[key];
|
|
156
368
|
Object.assign(data, freshData);
|
|
@@ -177,7 +389,7 @@ function createYrestStorage(filePath) {
|
|
|
177
389
|
}
|
|
178
390
|
};
|
|
179
391
|
}
|
|
180
|
-
var import_node_fs3, import_node_path2, import_node_crypto2,
|
|
392
|
+
var import_node_fs3, import_node_path2, import_node_crypto2, import_yaml3;
|
|
181
393
|
var init_yrestStorage = __esm({
|
|
182
394
|
"src/storage/yrestStorage.ts"() {
|
|
183
395
|
"use strict";
|
|
@@ -185,8 +397,10 @@ var init_yrestStorage = __esm({
|
|
|
185
397
|
import_node_fs3 = require("fs");
|
|
186
398
|
import_node_path2 = require("path");
|
|
187
399
|
import_node_crypto2 = require("crypto");
|
|
188
|
-
|
|
400
|
+
import_yaml3 = require("yaml");
|
|
189
401
|
init_parseRelations();
|
|
402
|
+
init_parseRoutes();
|
|
403
|
+
init_parseSchema();
|
|
190
404
|
init_deepCopy();
|
|
191
405
|
}
|
|
192
406
|
});
|
|
@@ -250,6 +464,7 @@ function dedent(str) {
|
|
|
250
464
|
init_cjs_shims();
|
|
251
465
|
var import_node_path3 = require("path");
|
|
252
466
|
init_parseRelations();
|
|
467
|
+
init_parseSchema();
|
|
253
468
|
|
|
254
469
|
// src/utils/handlers.ts
|
|
255
470
|
init_cjs_shims();
|
|
@@ -382,7 +597,7 @@ function endpointRow(method, path, desc) {
|
|
|
382
597
|
}
|
|
383
598
|
function resourceAccordion(name, base, isOpen) {
|
|
384
599
|
const p = `${base}/${name}`;
|
|
385
|
-
const
|
|
600
|
+
const singular2 = name.endsWith("s") ? name.slice(0, -1) : name;
|
|
386
601
|
const rows = [
|
|
387
602
|
endpointRow(
|
|
388
603
|
"GET",
|
|
@@ -392,20 +607,20 @@ function resourceAccordion(name, base, isOpen) {
|
|
|
392
607
|
endpointRow(
|
|
393
608
|
"POST",
|
|
394
609
|
p,
|
|
395
|
-
`Create a new ${
|
|
610
|
+
`Create a new ${singular2}. Auto-assigns <code>id</code> if not provided.`
|
|
396
611
|
),
|
|
397
|
-
endpointRow("GET", `${p}/:id`, `Get a single ${
|
|
612
|
+
endpointRow("GET", `${p}/:id`, `Get a single ${singular2} by id.`),
|
|
398
613
|
endpointRow(
|
|
399
614
|
"PUT",
|
|
400
615
|
`${p}/:id`,
|
|
401
|
-
`Fully replace a ${
|
|
616
|
+
`Fully replace a ${singular2}. Original <code>id</code> is always preserved.`
|
|
402
617
|
),
|
|
403
618
|
endpointRow(
|
|
404
619
|
"PATCH",
|
|
405
620
|
`${p}/:id`,
|
|
406
|
-
`Partially update a ${
|
|
621
|
+
`Partially update a ${singular2} \u2014 only provided fields change.`
|
|
407
622
|
),
|
|
408
|
-
endpointRow("DELETE", `${p}/:id`, `Delete a ${
|
|
623
|
+
endpointRow("DELETE", `${p}/:id`, `Delete a ${singular2} and return it as confirmation.`)
|
|
409
624
|
].join("");
|
|
410
625
|
return `
|
|
411
626
|
<details class="resource-card" ${isOpen ? "open" : ""}>
|
|
@@ -424,13 +639,13 @@ function nestedRoutesAccordion(relations, base) {
|
|
|
424
639
|
for (const [key, def] of Object.entries(fields)) {
|
|
425
640
|
const nestedBadge = def.nested ? ` ${badge("nested", "#facc15", "#facc1518")}` : "";
|
|
426
641
|
if (def.type === "many2many") {
|
|
427
|
-
const
|
|
642
|
+
const singular2 = source.endsWith("s") ? source.slice(0, -1) : source;
|
|
428
643
|
const m2mBadge = badge("many2many", "#818cf8", "#818cf818");
|
|
429
644
|
rows.push(
|
|
430
645
|
endpointRow(
|
|
431
646
|
"GET",
|
|
432
647
|
`${base}/${source}/:id/${key}`,
|
|
433
|
-
`List ${def.target} linked to a ${
|
|
648
|
+
`List ${def.target} linked to a ${singular2} via ${def.through}. ${m2mBadge}${nestedBadge}`
|
|
434
649
|
)
|
|
435
650
|
);
|
|
436
651
|
const targetSingular = def.target.endsWith("s") ? def.target.slice(0, -1) : def.target;
|
|
@@ -556,7 +771,7 @@ function examplesBlock(collections, relations, base, host, options, firstCustomR
|
|
|
556
771
|
const firstCol = collections[0];
|
|
557
772
|
if (firstCol) {
|
|
558
773
|
const p = `${host}${base}/${firstCol}`;
|
|
559
|
-
const
|
|
774
|
+
const singular2 = firstCol.endsWith("s") ? firstCol.slice(0, -1) : firstCol;
|
|
560
775
|
examples.push(
|
|
561
776
|
`# List all ${firstCol}
|
|
562
777
|
curl ${p}`,
|
|
@@ -564,17 +779,17 @@ curl ${p}`,
|
|
|
564
779
|
curl "${p}?name=value"`,
|
|
565
780
|
`# Sort and paginate
|
|
566
781
|
curl "${p}?_sort=id&_order=desc&_page=1&_limit=5"`,
|
|
567
|
-
`# Get single ${
|
|
782
|
+
`# Get single ${singular2}
|
|
568
783
|
curl ${p}/1`,
|
|
569
|
-
`# Create ${
|
|
784
|
+
`# Create ${singular2}
|
|
570
785
|
curl -X POST ${p} \\
|
|
571
786
|
-H "Content-Type: application/json" \\
|
|
572
787
|
-d '{"name":"example"}'`,
|
|
573
|
-
`# Partially update ${
|
|
788
|
+
`# Partially update ${singular2}
|
|
574
789
|
curl -X PATCH ${p}/1 \\
|
|
575
790
|
-H "Content-Type: application/json" \\
|
|
576
791
|
-d '{"name":"updated"}'`,
|
|
577
|
-
`# Delete ${
|
|
792
|
+
`# Delete ${singular2}
|
|
578
793
|
curl -X DELETE ${p}/1`
|
|
579
794
|
);
|
|
580
795
|
}
|
|
@@ -637,12 +852,510 @@ curl ${curlFlag}${fullPath}`);
|
|
|
637
852
|
return `<pre>${highlighted}</pre>`;
|
|
638
853
|
}
|
|
639
854
|
|
|
855
|
+
// src/router/templates/about.diagram.ts
|
|
856
|
+
init_cjs_shims();
|
|
857
|
+
function inferType(value) {
|
|
858
|
+
if (value === null || value === void 0) return "string";
|
|
859
|
+
if (typeof value === "boolean") return "boolean";
|
|
860
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
861
|
+
if (Array.isArray(value)) return "array";
|
|
862
|
+
if (typeof value === "object") return "object";
|
|
863
|
+
return "string";
|
|
864
|
+
}
|
|
865
|
+
function generateERData(storage) {
|
|
866
|
+
const data = storage.getData();
|
|
867
|
+
const relations = storage.getRelations();
|
|
868
|
+
const schema = storage.getSchema();
|
|
869
|
+
const pivotTables = /* @__PURE__ */ new Set();
|
|
870
|
+
for (const fields of Object.values(relations)) {
|
|
871
|
+
for (const def of Object.values(fields)) {
|
|
872
|
+
if (def.type === "many2many") pivotTables.add(def.through);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const fkFields = /* @__PURE__ */ new Map();
|
|
876
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
877
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
878
|
+
if (def.type !== "many2many") {
|
|
879
|
+
if (!fkFields.has(source)) fkFields.set(source, /* @__PURE__ */ new Set());
|
|
880
|
+
fkFields.get(source).add(key);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const entities = Object.entries(data).map(([name, items]) => {
|
|
885
|
+
const sample = items[0] ?? {};
|
|
886
|
+
const schemaFields = schema[name] ?? {};
|
|
887
|
+
const allFields = /* @__PURE__ */ new Set([...Object.keys(sample), ...Object.keys(schemaFields)]);
|
|
888
|
+
const fields = [...allFields].map((fieldName) => ({
|
|
889
|
+
name: fieldName,
|
|
890
|
+
type: schemaFields[fieldName]?.type ?? inferType(sample[fieldName]),
|
|
891
|
+
pk: fieldName === "id",
|
|
892
|
+
fk: fkFields.get(name)?.has(fieldName) ?? false,
|
|
893
|
+
required: schemaFields[fieldName]?.required ?? false
|
|
894
|
+
}));
|
|
895
|
+
fields.sort((a, b) => {
|
|
896
|
+
if (a.pk) return -1;
|
|
897
|
+
if (b.pk) return 1;
|
|
898
|
+
if (a.fk && !b.fk) return 1;
|
|
899
|
+
if (!a.fk && b.fk) return -1;
|
|
900
|
+
return 0;
|
|
901
|
+
});
|
|
902
|
+
return { name, fields, isPivot: pivotTables.has(name) };
|
|
903
|
+
});
|
|
904
|
+
const erRelations = [];
|
|
905
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
906
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
907
|
+
if (def.type === "many2many") {
|
|
908
|
+
erRelations.push({
|
|
909
|
+
source,
|
|
910
|
+
target: def.target,
|
|
911
|
+
type: "many2many",
|
|
912
|
+
label: def.through,
|
|
913
|
+
through: def.through,
|
|
914
|
+
foreignKey: def.foreignKey,
|
|
915
|
+
otherKey: def.otherKey
|
|
916
|
+
});
|
|
917
|
+
} else {
|
|
918
|
+
erRelations.push({
|
|
919
|
+
source,
|
|
920
|
+
target: def.target,
|
|
921
|
+
type: def.type,
|
|
922
|
+
label: key,
|
|
923
|
+
carDirect: def.carDirect,
|
|
924
|
+
carInverse: def.carInverse
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return { entities, relations: erRelations };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/router/templates/about.diagram.canvas.ts
|
|
933
|
+
init_cjs_shims();
|
|
934
|
+
function erDiagramSection(data) {
|
|
935
|
+
if (!data.entities.length) return "";
|
|
936
|
+
const json = JSON.stringify(data);
|
|
937
|
+
return (
|
|
938
|
+
/*html*/
|
|
939
|
+
`
|
|
940
|
+
<h2>Data Model</h2>
|
|
941
|
+
|
|
942
|
+
<div class="card" style="padding:0;overflow:hidden;position:relative;">
|
|
943
|
+
<canvas id="er-canvas" style="width:100%;display:block;cursor:grab;"></canvas>
|
|
944
|
+
<button id="er-dl" title="Download PNG" style="position:absolute;top:10px;right:14px;background:#161b22;border:1px solid #30363d;color:#7d8590;padding:4px 10px;border-radius:6px;font-size:11px;cursor:pointer;line-height:1.4;">\u2193 PNG</button>
|
|
945
|
+
</div>
|
|
946
|
+
|
|
947
|
+
<script>${/*js*/
|
|
948
|
+
`
|
|
949
|
+
(function(){
|
|
950
|
+
|
|
951
|
+
var D=${json};
|
|
952
|
+
|
|
953
|
+
// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
954
|
+
var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=8,BO=14,LGH=56;
|
|
955
|
+
|
|
956
|
+
var C={bg:'#0d1117',nb:'#161b22',nh:'#1c2128',pb:'#0d1117',ph:'#161b22',
|
|
957
|
+
bo:'#30363d',bs:'#58a6ff',tx:'#e6edf3',mu:'#7d8590',
|
|
958
|
+
pk:'#e3b341',fk:'#79c0ff',m2o:'#58a6ff',o2o:'#34d399',m2m:'#818cf8'};
|
|
959
|
+
|
|
960
|
+
// \u2500\u2500 Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
961
|
+
var cv = document.getElementById('er-canvas');
|
|
962
|
+
var dpr = window.devicePixelRatio||1;
|
|
963
|
+
var ctx = cv.getContext('2d');
|
|
964
|
+
|
|
965
|
+
function resize(h){
|
|
966
|
+
if(h)cv.style.height = h + 'px';
|
|
967
|
+
var r = cv.getBoundingClientRect();
|
|
968
|
+
cv.width = Math.round(r.width*dpr);
|
|
969
|
+
cv.height = Math.round(r.height*dpr);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// \u2500\u2500 Nodes & adjacency \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
973
|
+
var nodes = D.entities.map(function(e){
|
|
974
|
+
return{e:e,x:0,y:0,w:NW,h:HH+e.fields.length*FH+6};
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
var nIdx={};D.entities.forEach(function(e,i){nIdx[e.name]=i;});
|
|
978
|
+
|
|
979
|
+
var adj=nodes.map(function(){return[];}); // forward
|
|
980
|
+
var radj=nodes.map(function(){return[];}); // reverse
|
|
981
|
+
|
|
982
|
+
D.relations.forEach(function(r){
|
|
983
|
+
if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
|
|
984
|
+
var s=nIdx[r.source],p=nIdx[r.through],t=nIdx[r.target];
|
|
985
|
+
if(s!=null&&p!=null&&s!==p){adj[s].push(p);radj[p].push(s);}
|
|
986
|
+
if(t!=null&&p!=null&&t!==p){adj[t].push(p);radj[p].push(t);}
|
|
987
|
+
}else{
|
|
988
|
+
var s=nIdx[r.source],t=nIdx[r.target];
|
|
989
|
+
if(s==null||t==null||s===t)return;
|
|
990
|
+
adj[s].push(t);radj[t].push(s);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// \u2500\u2500 Layered layout (simplified Sugiyama) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
995
|
+
var groups=[];
|
|
996
|
+
|
|
997
|
+
function median(arr){
|
|
998
|
+
if(!arr.length)return 0;
|
|
999
|
+
var s=[].concat(arr).sort(function(a,b){return a-b;});
|
|
1000
|
+
return s[Math.floor(s.length/2)];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function computeLayout(W,H){
|
|
1004
|
+
var layer=nodes.map(function(){return 0;});
|
|
1005
|
+
var inDeg=nodes.map(function(){return 0;});
|
|
1006
|
+
adj.forEach(function(ts){ts.forEach(function(t){inDeg[t]++;});});
|
|
1007
|
+
var q=[],qi=0;
|
|
1008
|
+
for(var i=0;i<nodes.length;i++)if(inDeg[i]===0)q.push(i);
|
|
1009
|
+
if(!q.length)for(var i=0;i<nodes.length;i++)q.push(i);
|
|
1010
|
+
while(qi<q.length){
|
|
1011
|
+
var u=q[qi++];
|
|
1012
|
+
adj[u].forEach(function(v){if(layer[v]<=layer[u]){layer[v]=layer[u]+1;q.push(v);}});
|
|
1013
|
+
}
|
|
1014
|
+
var maxL=layer.reduce(function(m,l){return Math.max(m,l);},0);
|
|
1015
|
+
groups=[];
|
|
1016
|
+
for(var l=0;l<=maxL;l++)groups.push([]);
|
|
1017
|
+
for(var i=0;i<nodes.length;i++)groups[layer[i]].push(i);
|
|
1018
|
+
for(var l=1;l<=maxL;l++){
|
|
1019
|
+
var pp={};groups[l-1].forEach(function(i,p){pp[i]=p;});
|
|
1020
|
+
groups[l].sort(function(a,b){
|
|
1021
|
+
return median(radj[a].map(function(x){return pp[x]!=null?pp[x]:0;}))-
|
|
1022
|
+
median(radj[b].map(function(x){return pp[x]!=null?pp[x]:0;}));
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
for(var l=maxL-1;l>=0;l--){
|
|
1026
|
+
var np={};groups[l+1].forEach(function(i,p){np[i]=p;});
|
|
1027
|
+
groups[l].sort(function(a,b){
|
|
1028
|
+
return median(adj[a].map(function(x){return np[x]!=null?np[x]:0;}))-
|
|
1029
|
+
median(adj[b].map(function(x){return np[x]!=null?np[x]:0;}));
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
var colW=NW+COL_GAP;
|
|
1033
|
+
var totalW=(maxL+1)*colW-COL_GAP;
|
|
1034
|
+
var startX=Math.max(20,(W-totalW)/2);
|
|
1035
|
+
for(var l=0;l<=maxL;l++){
|
|
1036
|
+
var g=groups[l];
|
|
1037
|
+
var totalH=g.reduce(function(s,i){return s+nodes[i].h;},0)+(g.length-1)*ROW_GAP;
|
|
1038
|
+
var y=Math.max(20,(H-totalH)/2);
|
|
1039
|
+
g.forEach(function(i){nodes[i].x=startX+l*colW;nodes[i].y=y;y+=nodes[i].h+ROW_GAP;});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function requiredH(){
|
|
1044
|
+
return Math.max(340,Math.min(720,groups.reduce(function(m,g){
|
|
1045
|
+
return Math.max(m,g.reduce(function(s,i){return s+nodes[i].h;},0)+(g.length-1)*ROW_GAP+LGH+70);
|
|
1046
|
+
},300)));
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// \u2500\u2500 View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1050
|
+
var pan={x:0,y:0},zm=1;
|
|
1051
|
+
var origPos=[];
|
|
1052
|
+
|
|
1053
|
+
function savePos(){origPos=nodes.map(function(n){return{x:n.x,y:n.y};});}
|
|
1054
|
+
|
|
1055
|
+
function fitView(W,H){
|
|
1056
|
+
var x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;
|
|
1057
|
+
nodes.forEach(function(n){x0=Math.min(x0,n.x);y0=Math.min(y0,n.y);x1=Math.max(x1,n.x+n.w);y1=Math.max(y1,n.y+n.h);});
|
|
1058
|
+
var pad=28,gW=x1-x0+pad*2,gH=y1-y0+pad*2;
|
|
1059
|
+
zm=Math.min(W/gW,(H-LGH)/gH,1);
|
|
1060
|
+
pan.x=(W-gW*zm)/2+pad*zm-x0*zm;
|
|
1061
|
+
pan.y=((H-LGH)-gH*zm)/2+pad*zm-y0*zm;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// \u2500\u2500 Rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1065
|
+
function rr(x,y,w,h,r){
|
|
1066
|
+
if(ctx.roundRect){ctx.beginPath();ctx.roundRect(x,y,w,h,r);return;}
|
|
1067
|
+
ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);
|
|
1068
|
+
ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function edgePt(n,tx,ty){
|
|
1072
|
+
var cx=n.x+n.w/2,cy=n.y+n.h/2,dx=tx-cx,dy=ty-cy;
|
|
1073
|
+
if(!dx&&!dy)return{x:cx,y:cy};
|
|
1074
|
+
var s=Math.min((n.w/2)/Math.abs(dx||.001),(n.h/2)/Math.abs(dy||.001));
|
|
1075
|
+
return{x:cx+dx*s,y:cy+dy*s};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
var badges=[];
|
|
1079
|
+
|
|
1080
|
+
function pushBadge(x,y,ux,uy,many,col){
|
|
1081
|
+
badges.push({x:x,y:y,ux:ux,uy:uy,many:many,col:col});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Badge: solid colored circle; crow's foot (many) or double-bar (one) in dark ink inside.
|
|
1085
|
+
// ux,uy points outward from the node \u2014 badge is offset BO px into the gap.
|
|
1086
|
+
// Crow's foot tines open toward the node (-x in rotated frame), tip toward line (+x).
|
|
1087
|
+
function drawBadge(b){
|
|
1088
|
+
var bx=b.x+b.ux*BO,by=b.y+b.uy*BO;
|
|
1089
|
+
ctx.fillStyle=b.col;ctx.strokeStyle=b.col;ctx.lineWidth=1;
|
|
1090
|
+
ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.stroke();
|
|
1091
|
+
ctx.save();ctx.translate(bx,by);ctx.rotate(Math.atan2(b.uy,b.ux));
|
|
1092
|
+
ctx.strokeStyle=C.bg;ctx.lineWidth=1.4;ctx.lineCap='round';
|
|
1093
|
+
ctx.beginPath();
|
|
1094
|
+
if(b.many){
|
|
1095
|
+
ctx.moveTo(-5,-4);ctx.lineTo(5,0); // upper tine \u2192 tip
|
|
1096
|
+
ctx.moveTo(-5,4);ctx.lineTo(5,0); // lower tine \u2192 tip
|
|
1097
|
+
ctx.moveTo(-5,0);ctx.lineTo(5,0); // center line
|
|
1098
|
+
ctx.moveTo(-5,-4);ctx.lineTo(-5,4); // base bar (near node)
|
|
1099
|
+
}else{
|
|
1100
|
+
ctx.moveTo(-3,-5);ctx.lineTo(-3,5); // near bar
|
|
1101
|
+
ctx.moveTo(2,-5);ctx.lineTo(2,5); // far bar
|
|
1102
|
+
}
|
|
1103
|
+
ctx.stroke();
|
|
1104
|
+
ctx.restore();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function ecol(t){return t==='one2one'?C.o2o:t==='many2many'?C.m2m:C.m2o;}
|
|
1108
|
+
|
|
1109
|
+
// offset: pixels to shift perpendicular to the edge direction (for parallel edges)
|
|
1110
|
+
function drawEdgeLine(a,b,col,dashed,label,manyA,manyB,off){
|
|
1111
|
+
var p1=edgePt(a,b.x+b.w/2,b.y+b.h/2);
|
|
1112
|
+
var p2=edgePt(b,a.x+a.w/2,a.y+a.h/2);
|
|
1113
|
+
var dx=p2.x-p1.x,dy=p2.y-p1.y,d=Math.sqrt(dx*dx+dy*dy)||1;
|
|
1114
|
+
var ux=dx/d,uy=dy/d;
|
|
1115
|
+
var px=-uy*off,py=ux*off; // perpendicular shift
|
|
1116
|
+
var q1x=p1.x+px,q1y=p1.y+py,q2x=p2.x+px,q2y=p2.y+py;
|
|
1117
|
+
ctx.strokeStyle=col+'55';ctx.lineWidth=1.5;
|
|
1118
|
+
ctx.setLineDash(dashed?[5,4]:[]);
|
|
1119
|
+
ctx.beginPath();ctx.moveTo(q1x,q1y);ctx.lineTo(q2x,q2y);ctx.stroke();
|
|
1120
|
+
ctx.setLineDash([]);
|
|
1121
|
+
pushBadge(q1x,q1y,ux,uy,manyA,col);
|
|
1122
|
+
pushBadge(q2x,q2y,-ux,-uy,manyB,col);
|
|
1123
|
+
if(label){
|
|
1124
|
+
var mx=(q1x+q2x)/2,my=(q1y+q2y)/2;
|
|
1125
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
|
|
1126
|
+
ctx.fillText(label,mx-uy*14,my+ux*14);
|
|
1127
|
+
ctx.textBaseline='alphabetic';
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// canonical key for a node pair regardless of direction
|
|
1132
|
+
function pairKey(a,b){
|
|
1133
|
+
var ai=nodes.indexOf(a),bi=nodes.indexOf(b);
|
|
1134
|
+
return ai<bi?ai+'-'+bi:bi+'-'+ai;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function collectEdges(){
|
|
1138
|
+
var edges=[];
|
|
1139
|
+
D.relations.forEach(function(r){
|
|
1140
|
+
var col=ecol(r.type);
|
|
1141
|
+
if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
|
|
1142
|
+
var sn=nodes[nIdx[r.source]],pn=nodes[nIdx[r.through]],tn=nodes[nIdx[r.target]];
|
|
1143
|
+
if(sn&&pn)edges.push({a:sn,b:pn,col:col,dashed:true,label:r.foreignKey||'',manyA:false,manyB:true,off:0});
|
|
1144
|
+
if(tn&&pn)edges.push({a:tn,b:pn,col:col,dashed:true,label:r.otherKey||'',manyA:false,manyB:true,off:0});
|
|
1145
|
+
}else{
|
|
1146
|
+
var sn=nodes[nIdx[r.source]],tn=nodes[nIdx[r.target]];
|
|
1147
|
+
if(sn&&tn&&sn!==tn){
|
|
1148
|
+
var manyA=r.carInverse?r.carInverse.endsWith('n'):r.type!=='one2one';
|
|
1149
|
+
var manyB=r.carDirect?r.carDirect.endsWith('n'):r.type==='many2many';
|
|
1150
|
+
edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,manyA:manyA,manyB:manyB,off:0});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
// assign perpendicular offsets to parallel edges (same node pair)
|
|
1155
|
+
var cnt={},idx={};
|
|
1156
|
+
edges.forEach(function(e){var k=pairKey(e.a,e.b);cnt[k]=(cnt[k]||0)+1;});
|
|
1157
|
+
edges.forEach(function(e){
|
|
1158
|
+
var k=pairKey(e.a,e.b),n=cnt[k];
|
|
1159
|
+
var i=idx[k]=(idx[k]||0);
|
|
1160
|
+
e.off=n>1?(i-(n-1)/2)*16:0;
|
|
1161
|
+
idx[k]++;
|
|
1162
|
+
});
|
|
1163
|
+
return edges;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function drawNode(n){
|
|
1167
|
+
var x=n.x,y=n.y,w=n.w,h=n.h,e=n.e;
|
|
1168
|
+
var bg=e.isPivot?C.pb:C.nb,hd=e.isPivot?C.ph:C.nh;
|
|
1169
|
+
ctx.save();ctx.shadowColor='rgba(0,0,0,0.45)';ctx.shadowBlur=14;ctx.shadowOffsetY=4;
|
|
1170
|
+
ctx.fillStyle=bg;rr(x,y,w,h,7);ctx.fill();ctx.restore();
|
|
1171
|
+
ctx.fillStyle=bg;rr(x,y,w,h,7);ctx.fill();
|
|
1172
|
+
ctx.strokeStyle=n===drag?C.bs:C.bo;ctx.lineWidth=1.2;rr(x,y,w,h,7);ctx.stroke();
|
|
1173
|
+
ctx.fillStyle=hd;rr(x,y,w,HH,7);ctx.fill();ctx.fillRect(x,y+HH-7,w,7);
|
|
1174
|
+
ctx.strokeStyle=C.bo;ctx.lineWidth=1;
|
|
1175
|
+
ctx.beginPath();ctx.moveTo(x,y+HH);ctx.lineTo(x+w,y+HH);ctx.stroke();
|
|
1176
|
+
ctx.fillStyle=e.isPivot?C.mu:C.tx;
|
|
1177
|
+
ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
|
|
1178
|
+
ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1179
|
+
ctx.fillText(e.name,x+FP,y+HH/2);
|
|
1180
|
+
if(e.isPivot){
|
|
1181
|
+
ctx.fillStyle='#3d444d';ctx.font='9px monospace';ctx.textAlign='right';
|
|
1182
|
+
ctx.fillText('pivot',x+w-FP,y+HH/2);
|
|
1183
|
+
}
|
|
1184
|
+
e.fields.forEach(function(f,fi){
|
|
1185
|
+
var fy=y+HH+fi*FH+FH/2+3;
|
|
1186
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
|
|
1187
|
+
ctx.fillText(f.type,x+w-FP-(f.required&&!f.pk?14:0),fy);
|
|
1188
|
+
if(f.pk||f.fk){
|
|
1189
|
+
var tag=f.pk?'PK':'FK',tc=f.pk?C.pk:C.fk;
|
|
1190
|
+
ctx.fillStyle=tc+'22';ctx.strokeStyle=tc+'66';ctx.lineWidth=1;
|
|
1191
|
+
rr(x+FP,fy-6,12,12,2);ctx.fill();ctx.stroke();
|
|
1192
|
+
ctx.fillStyle=tc;ctx.font='7px monospace';ctx.textAlign='center';
|
|
1193
|
+
ctx.fillText(tag,x+FP+6,fy+0.5);
|
|
1194
|
+
}
|
|
1195
|
+
ctx.fillStyle=f.pk?C.pk:f.fk?C.fk:C.tx;
|
|
1196
|
+
ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
|
|
1197
|
+
ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1198
|
+
ctx.fillText(f.name,x+FP+(f.pk||f.fk?16:0),fy);
|
|
1199
|
+
if(f.required&&!f.pk){
|
|
1200
|
+
ctx.fillStyle='#f85149';ctx.beginPath();
|
|
1201
|
+
ctx.arc(x+w-FP-6,fy,2.5,0,Math.PI*2);ctx.fill();
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
ctx.textBaseline='alphabetic';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function drawLegend(){
|
|
1208
|
+
var cvW=cv.width/dpr,cvH=cv.height/dpr;
|
|
1209
|
+
var y0=cvH-LGH,y1=cvH-LGH+18,y2=cvH-LGH+38;
|
|
1210
|
+
|
|
1211
|
+
ctx.fillStyle='#161b22';
|
|
1212
|
+
ctx.fillRect(0,y0,cvW,LGH);
|
|
1213
|
+
ctx.strokeStyle='#30363d';ctx.lineWidth=1;
|
|
1214
|
+
ctx.beginPath();ctx.moveTo(0,y0);ctx.lineTo(cvW,y0);ctx.stroke();
|
|
1215
|
+
|
|
1216
|
+
// Row 1: relation line types
|
|
1217
|
+
var types=[
|
|
1218
|
+
{c:C.m2o,l:'many2one',d:false},
|
|
1219
|
+
{c:C.o2o,l:'one2one',d:false},
|
|
1220
|
+
{c:C.m2m,l:'many2many',d:true}
|
|
1221
|
+
];
|
|
1222
|
+
ctx.textAlign='left';ctx.textBaseline='middle';ctx.font='10px monospace';
|
|
1223
|
+
var x=14;
|
|
1224
|
+
types.forEach(function(it){
|
|
1225
|
+
ctx.strokeStyle=it.c;ctx.lineWidth=1.5;ctx.setLineDash(it.d?[4,3]:[]);
|
|
1226
|
+
ctx.beginPath();ctx.moveTo(x,y1);ctx.lineTo(x+14,y1);ctx.stroke();
|
|
1227
|
+
ctx.setLineDash([]);
|
|
1228
|
+
ctx.fillStyle=C.mu;
|
|
1229
|
+
ctx.fillText(it.l,x+18,y1);
|
|
1230
|
+
x+=18+ctx.measureText(it.l).width+22;
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
// Row 2: cardinality symbols
|
|
1234
|
+
function badge2(cx,cy,many,col){
|
|
1235
|
+
ctx.fillStyle=col;ctx.strokeStyle=col;ctx.lineWidth=1;
|
|
1236
|
+
ctx.beginPath();ctx.arc(cx,cy,6,0,Math.PI*2);ctx.fill();
|
|
1237
|
+
ctx.save();ctx.translate(cx,cy);
|
|
1238
|
+
ctx.strokeStyle=C.bg;ctx.lineWidth=1.2;ctx.lineCap='round';
|
|
1239
|
+
ctx.beginPath();
|
|
1240
|
+
if(many){
|
|
1241
|
+
ctx.moveTo(-4,-3);ctx.lineTo(4,0);ctx.moveTo(-4,3);ctx.lineTo(4,0);
|
|
1242
|
+
ctx.moveTo(-4,0);ctx.lineTo(4,0);ctx.moveTo(-4,-3);ctx.lineTo(-4,3);
|
|
1243
|
+
}else{
|
|
1244
|
+
ctx.moveTo(-2,-3);ctx.lineTo(-2,3);ctx.moveTo(2,-3);ctx.lineTo(2,3);
|
|
1245
|
+
}
|
|
1246
|
+
ctx.stroke();ctx.restore();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1250
|
+
x=14;
|
|
1251
|
+
var pairs=[
|
|
1252
|
+
{a:false,b:true,col:C.m2o,l:'many2one (N:1)'},
|
|
1253
|
+
{a:false,b:false,col:C.o2o,l:'one2one (1:1)'},
|
|
1254
|
+
{a:false,b:true,col:C.m2m,l:'pivot edge (1:N)'}
|
|
1255
|
+
];
|
|
1256
|
+
pairs.forEach(function(p){
|
|
1257
|
+
ctx.strokeStyle=p.col+'55';ctx.lineWidth=1.2;ctx.setLineDash(p.col===C.m2m?[4,3]:[]);
|
|
1258
|
+
ctx.beginPath();ctx.moveTo(x+8,y2);ctx.lineTo(x+56,y2);ctx.stroke();
|
|
1259
|
+
ctx.setLineDash([]);
|
|
1260
|
+
badge2(x+8,y2,p.a,p.col);
|
|
1261
|
+
badge2(x+56,y2,p.b,p.col);
|
|
1262
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1263
|
+
ctx.fillText(p.l,x+66,y2);
|
|
1264
|
+
x+=66+ctx.measureText(p.l).width+24;
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
ctx.fillStyle='#3d444d';ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
|
|
1268
|
+
ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y2);
|
|
1269
|
+
ctx.textBaseline='alphabetic';
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function draw(){
|
|
1273
|
+
var W=cv.width/dpr,H=cv.height/dpr;
|
|
1274
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
1275
|
+
ctx.clearRect(0,0,W,H);
|
|
1276
|
+
ctx.fillStyle=C.bg;ctx.fillRect(0,0,W,H);
|
|
1277
|
+
ctx.save();ctx.translate(pan.x,pan.y);ctx.scale(zm,zm);
|
|
1278
|
+
badges=[];
|
|
1279
|
+
var edges=collectEdges();
|
|
1280
|
+
edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.manyA,e.manyB,e.off);});
|
|
1281
|
+
nodes.forEach(function(n){drawNode(n);});
|
|
1282
|
+
badges.forEach(function(b){drawBadge(b);});
|
|
1283
|
+
ctx.restore();
|
|
1284
|
+
drawLegend();
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// \u2500\u2500 Interaction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1288
|
+
var drag=null,dox=0,doy=0,isPan=false,ps={x:0,y:0};
|
|
1289
|
+
|
|
1290
|
+
function toW(mx,my){return{x:(mx-pan.x)/zm,y:(my-pan.y)/zm};}
|
|
1291
|
+
function hitTest(wx,wy){
|
|
1292
|
+
for(var i=nodes.length-1;i>=0;i--){var n=nodes[i];if(wx>=n.x&&wx<=n.x+n.w&&wy>=n.y&&wy<=n.y+n.h)return n;}
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
cv.addEventListener('mousedown',function(ev){
|
|
1297
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1298
|
+
var w=toW(mx,my),h=hitTest(w.x,w.y);
|
|
1299
|
+
if(h){drag=h;dox=w.x-h.x;doy=w.y-h.y;}else{isPan=true;ps={x:mx-pan.x,y:my-pan.y};}
|
|
1300
|
+
cv.style.cursor='grabbing';
|
|
1301
|
+
});
|
|
1302
|
+
window.addEventListener('mousemove',function(ev){
|
|
1303
|
+
if(!drag&&!isPan)return;
|
|
1304
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1305
|
+
if(drag){var w=toW(mx,my);drag.x=w.x-dox;drag.y=w.y-doy;}
|
|
1306
|
+
else{pan.x=mx-ps.x;pan.y=my-ps.y;}
|
|
1307
|
+
draw();
|
|
1308
|
+
});
|
|
1309
|
+
window.addEventListener('mouseup',function(){drag=null;isPan=false;cv.style.cursor='grab';});
|
|
1310
|
+
cv.addEventListener('wheel',function(ev){
|
|
1311
|
+
ev.preventDefault();
|
|
1312
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1313
|
+
var d=ev.deltaY<0?1.1:0.91,nz=Math.max(0.25,Math.min(3,zm*d));
|
|
1314
|
+
pan.x=mx-(mx-pan.x)*(nz/zm);pan.y=my-(my-pan.y)*(nz/zm);zm=nz;draw();
|
|
1315
|
+
},{passive:false});
|
|
1316
|
+
cv.addEventListener('dblclick',function(){
|
|
1317
|
+
nodes.forEach(function(n,i){n.x=origPos[i].x;n.y=origPos[i].y;});
|
|
1318
|
+
var r=cv.getBoundingClientRect();fitView(r.width,r.height);draw();
|
|
1319
|
+
});
|
|
1320
|
+
window.addEventListener('resize',function(){
|
|
1321
|
+
resize();var r=cv.getBoundingClientRect();
|
|
1322
|
+
computeLayout(r.width,r.height);savePos();fitView(r.width,r.height);draw();
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
document.getElementById('er-dl').addEventListener('click',function(){
|
|
1326
|
+
var SCALE=3;
|
|
1327
|
+
var cw=cv.width/dpr,ch=cv.height/dpr;
|
|
1328
|
+
var oc=document.createElement('canvas');
|
|
1329
|
+
oc.width=Math.round(cw*SCALE);oc.height=Math.round(ch*SCALE);
|
|
1330
|
+
var sav={cv:cv,ctx:ctx,dpr:dpr};
|
|
1331
|
+
cv=oc;ctx=oc.getContext('2d');dpr=SCALE;
|
|
1332
|
+
draw();
|
|
1333
|
+
cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;
|
|
1334
|
+
var a=document.createElement('a');
|
|
1335
|
+
a.download='er-diagram.png';a.href=oc.toDataURL('image/png');a.click();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1339
|
+
requestAnimationFrame(function(){
|
|
1340
|
+
resize();
|
|
1341
|
+
var r=cv.getBoundingClientRect();
|
|
1342
|
+
computeLayout(r.width,r.height);
|
|
1343
|
+
var h=requiredH();
|
|
1344
|
+
resize(h);r=cv.getBoundingClientRect();
|
|
1345
|
+
computeLayout(r.width,r.height);
|
|
1346
|
+
savePos();fitView(r.width,r.height);draw();
|
|
1347
|
+
});
|
|
1348
|
+
})();
|
|
1349
|
+
`}</script>`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
640
1353
|
// src/router/templates/about.template.ts
|
|
641
1354
|
var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
642
1355
|
var LOGO_SRC = (() => {
|
|
643
1356
|
try {
|
|
644
|
-
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/
|
|
645
|
-
return `data:image/
|
|
1357
|
+
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/yrest-lockup.svg"));
|
|
1358
|
+
return `data:image/svg+xml;base64,${buf.toString("base64")}`;
|
|
646
1359
|
} catch {
|
|
647
1360
|
return "";
|
|
648
1361
|
}
|
|
@@ -653,6 +1366,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
653
1366
|
const customRoutes = storage.getRoutes();
|
|
654
1367
|
const base = options.base;
|
|
655
1368
|
const host = `http://${options.host}:${options.port}`;
|
|
1369
|
+
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
656
1370
|
const modes = [];
|
|
657
1371
|
if (options.watch) modes.push(badge("watch", "#38bdf8", "#38bdf818"));
|
|
658
1372
|
if (options.readonly) modes.push(badge("readonly", "#94a3b8", "#94a3b818"));
|
|
@@ -663,9 +1377,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
663
1377
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
664
1378
|
if (options.idStrategy !== "increment")
|
|
665
1379
|
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
1380
|
+
const erDiagram = erDiagramSection(generateERData(storage));
|
|
666
1381
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
667
1382
|
const paginationDesc = options.pageable.enabled ? `Pageable mode active \u2014 default limit <code>${options.pageable.limit}</code>. Response wrapped in <code>{ data, pagination }</code>.` : `Returns the requested slice. <code>X-Total-Count</code> header reflects the total before pagination.`;
|
|
668
|
-
return
|
|
1383
|
+
return (
|
|
1384
|
+
/*html*/
|
|
1385
|
+
`<!DOCTYPE html>
|
|
669
1386
|
<html lang="en">
|
|
670
1387
|
<head>
|
|
671
1388
|
<meta charset="UTF-8">
|
|
@@ -692,20 +1409,30 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
692
1409
|
width: 100%;
|
|
693
1410
|
background: linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a2332 100%);
|
|
694
1411
|
border-bottom: 1px solid var(--border);
|
|
695
|
-
padding:
|
|
1412
|
+
padding: 20px 32px 16px;
|
|
696
1413
|
}
|
|
697
|
-
.banner-inner {
|
|
698
|
-
.banner
|
|
1414
|
+
.banner-inner { margin: 0 auto; display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; }
|
|
1415
|
+
.banner-left { flex:1; min-width:0; }
|
|
1416
|
+
.banner h1 { font-size: clamp(24px, 4vw, 40px); font-weight: 800; letter-spacing: -1.5px; line-height: 1; }
|
|
699
1417
|
.banner h1 .y { color: var(--text); }
|
|
700
1418
|
.banner h1 .rest { color: var(--accent); }
|
|
701
|
-
.banner p { color: var(--text-muted); margin-top:
|
|
702
|
-
.banner-meta { display: flex; gap:
|
|
703
|
-
.banner-meta span { color: var(--text-muted); font-size:
|
|
1419
|
+
.banner p { color: var(--text-muted); margin-top: 6px; font-size: 14px; }
|
|
1420
|
+
.banner-meta { display: flex; gap: 20px; margin-top: 10px; flex-wrap: wrap; }
|
|
1421
|
+
.banner-meta span { color: var(--text-muted); font-size: 12px; }
|
|
704
1422
|
.banner-meta span strong { color: var(--text); font-family: monospace; }
|
|
1423
|
+
.api-links { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
|
1424
|
+
.btn-api { display:inline-block; padding:5px 12px; border-radius:6px; border:1px solid var(--border); color:var(--text-muted); text-decoration:none; font-size:12px; font-weight:500; background:var(--bg-card); white-space:nowrap; transition:border-color .15s,color .15s; }
|
|
1425
|
+
.btn-api:hover { border-color:var(--border-hi); color:var(--text); }
|
|
1426
|
+
.btn-swagger { border-color:var(--accent); color:var(--accent); }
|
|
1427
|
+
.btn-swagger:hover { background:#58a6ff18; }
|
|
705
1428
|
|
|
706
1429
|
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
707
|
-
.wrap {
|
|
1430
|
+
.wrap { padding: 24px 32px 48px; }
|
|
708
1431
|
h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin: 32px 0 12px; }
|
|
1432
|
+
.about-grid { display:grid; grid-template-columns:5fr 7fr; gap:28px; align-items:start; }
|
|
1433
|
+
@media(max-width:960px) { .about-grid { grid-template-columns:1fr; } }
|
|
1434
|
+
.col-left, .col-right { min-width:0; }
|
|
1435
|
+
.col-left > h2:first-child, .col-right > h2:first-child { margin-top:0; }
|
|
709
1436
|
|
|
710
1437
|
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
711
1438
|
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; }
|
|
@@ -721,10 +1448,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
721
1448
|
.modes { display: flex; gap: 8px; flex-wrap: wrap; min-height: 28px; align-items: center; }
|
|
722
1449
|
.badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
723
1450
|
|
|
724
|
-
/* \u2500\u2500 Endpoints
|
|
725
|
-
.endpoints-grid { display:
|
|
726
|
-
@media (max-width: 860px) { .endpoints-grid { grid-template-columns: 1fr; } }
|
|
727
|
-
.nested-card { grid-column: 1 / -1; }
|
|
1451
|
+
/* \u2500\u2500 Endpoints list \u2500\u2500 */
|
|
1452
|
+
.endpoints-grid { display: flex; flex-direction: column; gap: 10px; }
|
|
728
1453
|
|
|
729
1454
|
/* \u2500\u2500 Accordion (details/summary) \u2500\u2500 */
|
|
730
1455
|
.resource-card {
|
|
@@ -789,56 +1514,71 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
789
1514
|
|
|
790
1515
|
<div class="banner">
|
|
791
1516
|
<div class="banner-inner">
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
<
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1517
|
+
<div class="banner-left">
|
|
1518
|
+
${LOGO_SRC ? `<img src="${LOGO_SRC}" alt="yRest" height="36" style="display:block;margin-bottom:4px" />` : `<h1><span class="y">y</span><span class="rest">Rest</span></h1>`}
|
|
1519
|
+
<p>Zero-config REST API mock server</p>
|
|
1520
|
+
<div class="banner-meta">
|
|
1521
|
+
<span>URL <strong>${host}</strong></span>
|
|
1522
|
+
<span>Base <strong>${base || "/"}</strong></span>
|
|
1523
|
+
<span>File <strong>${options.file}</strong></span>
|
|
1524
|
+
<span>Collections <strong>${collections.length}</strong></span>
|
|
1525
|
+
</div>
|
|
1526
|
+
</div>
|
|
1527
|
+
<div class="api-links">
|
|
1528
|
+
<a href="/_openapi" target="_blank" class="btn-api">OpenAPI YAML</a>
|
|
1529
|
+
<a href="/_openapi.json" target="_blank" class="btn-api">OpenAPI JSON</a>
|
|
1530
|
+
<a href="${swaggerUrl}" target="_blank" class="btn-api btn-swagger">Swagger UI \u2197</a>
|
|
799
1531
|
</div>
|
|
800
1532
|
</div>
|
|
801
1533
|
</div>
|
|
802
1534
|
|
|
803
1535
|
<div class="wrap">
|
|
1536
|
+
<div class="about-grid">
|
|
804
1537
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1538
|
+
<div class="col-left">
|
|
1539
|
+
<h2>Active Modes</h2>
|
|
1540
|
+
<div class="card">
|
|
1541
|
+
<div class="modes">${modes.length ? modes.join(" ") : `<span style="color:var(--text-muted);font-size:13px">none</span>`}</div>
|
|
1542
|
+
${options.watch ? `<div class="warn">\u26A0 <strong>Watch mode:</strong> data changes in existing collections reload automatically. Adding or removing entire collections requires a server restart.</div>` : ""}
|
|
1543
|
+
</div>
|
|
810
1544
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1545
|
+
<h2>Endpoints</h2>
|
|
1546
|
+
<div class="endpoints-grid">
|
|
1547
|
+
${accordions}
|
|
1548
|
+
${nestedRoutesAccordion(relations, base)}
|
|
1549
|
+
${options.snapshot ? snapshotAccordion() : ""}
|
|
1550
|
+
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
1551
|
+
${handlersAccordion(handlers, customRoutes, base)}
|
|
1552
|
+
</div>
|
|
819
1553
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1554
|
+
<h2>Query Parameters</h2>
|
|
1555
|
+
<div class="card">
|
|
1556
|
+
<table class="param-table">
|
|
1557
|
+
<thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
|
|
1558
|
+
<tbody>
|
|
1559
|
+
<tr><td><code>?field=value</code></td><td><code>?name=Ana&role=admin</code></td><td>Filter by any field. Multiple params are ANDed.</td></tr>
|
|
1560
|
+
<tr><td><code>?field_gte / _lte</code></td><td><code>?price_gte=10&price_lte=50</code></td><td>Numeric or lexicographic range. Works with any comparable field.</td></tr>
|
|
1561
|
+
<tr><td><code>?field_ne</code></td><td><code>?status_ne=inactive</code></td><td>Exclude items where the field equals the value.</td></tr>
|
|
1562
|
+
<tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
|
|
1563
|
+
<tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
|
|
1564
|
+
<tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
|
|
1565
|
+
<tr><td><code>?_q</code></td><td><code>?_q=ana</code></td><td>Full-text search across all scalar fields (case-insensitive substring).</td></tr>
|
|
1566
|
+
<tr><td><code>?_sort & ?_order</code></td><td><code>?_sort=name&_order=desc</code></td><td>Sort by field. <code>_order</code>: <code>asc</code> (default) or <code>desc</code>.</td></tr>
|
|
1567
|
+
<tr><td><code>?_page & ?_limit</code></td><td><code>?_page=2&_limit=10</code></td><td>${paginationDesc}</td></tr>
|
|
1568
|
+
<tr><td><code>?_expand</code></td><td><code>?_expand=user</code></td><td>Embed a related parent object inline. Requires <code>_rel</code> in the YAML file.</td></tr>
|
|
1569
|
+
<tr><td><code>?_embed</code></td><td><code>?_embed=posts</code></td><td>Embed child collections into each parent item. Requires <code>_rel</code> in the YAML file.</td></tr>
|
|
1570
|
+
<tr><td><code>?_fields</code></td><td><code>?_fields=id,name</code></td><td>Return only the specified fields. Applied last \u2014 can include embedded/expanded keys.</td></tr>
|
|
1571
|
+
</tbody>
|
|
1572
|
+
</table>
|
|
1573
|
+
</div>
|
|
1574
|
+
</div>
|
|
1575
|
+
|
|
1576
|
+
<div class="col-right">
|
|
1577
|
+
${erDiagram}
|
|
1578
|
+
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
1579
|
+
</div>
|
|
840
1580
|
|
|
841
|
-
|
|
1581
|
+
</div>
|
|
842
1582
|
|
|
843
1583
|
<footer>
|
|
844
1584
|
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
@@ -846,7 +1586,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
846
1586
|
|
|
847
1587
|
</div>
|
|
848
1588
|
</body>
|
|
849
|
-
</html
|
|
1589
|
+
</html>`
|
|
1590
|
+
);
|
|
850
1591
|
}
|
|
851
1592
|
|
|
852
1593
|
// src/router/routes/about.routes.ts
|
|
@@ -1447,12 +2188,23 @@ var NestedRouteCommand = class {
|
|
|
1447
2188
|
relations;
|
|
1448
2189
|
base;
|
|
1449
2190
|
register(server) {
|
|
2191
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1450
2192
|
for (const [source, fields] of Object.entries(this.relations)) {
|
|
1451
2193
|
for (const [key, def] of Object.entries(fields)) {
|
|
1452
2194
|
if (def.type === "many2many") {
|
|
1453
|
-
this.
|
|
2195
|
+
const fwd = `${this.base}/${source}/:id/${key}`;
|
|
2196
|
+
const inv = `${this.base}/${def.target}/:id/${source}`;
|
|
2197
|
+
if (!registered.has(fwd) && !registered.has(inv)) {
|
|
2198
|
+
registered.add(fwd);
|
|
2199
|
+
registered.add(inv);
|
|
2200
|
+
this.registerMany2Many(server, source, key, def);
|
|
2201
|
+
}
|
|
1454
2202
|
} else {
|
|
1455
|
-
|
|
2203
|
+
const collPath = `${this.base}/${def.target}/:id/${source}`;
|
|
2204
|
+
if (!registered.has(collPath)) {
|
|
2205
|
+
registered.add(collPath);
|
|
2206
|
+
this.registerFkRelation(server, source, key, def.target, def.type);
|
|
2207
|
+
}
|
|
1456
2208
|
}
|
|
1457
2209
|
}
|
|
1458
2210
|
}
|
|
@@ -1511,6 +2263,379 @@ var NestedRouteCommand = class {
|
|
|
1511
2263
|
}
|
|
1512
2264
|
};
|
|
1513
2265
|
|
|
2266
|
+
// src/router/routes/openapi.routes.ts
|
|
2267
|
+
init_cjs_shims();
|
|
2268
|
+
|
|
2269
|
+
// src/openapi/generateOpenApi.ts
|
|
2270
|
+
init_cjs_shims();
|
|
2271
|
+
|
|
2272
|
+
// src/openapi/inferSchema.ts
|
|
2273
|
+
init_cjs_shims();
|
|
2274
|
+
function buildCollectionSchema(items, fieldDefs = {}) {
|
|
2275
|
+
const sample = items.slice(0, 10);
|
|
2276
|
+
const inferredTypes = /* @__PURE__ */ new Map();
|
|
2277
|
+
for (const item of sample) {
|
|
2278
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2279
|
+
if (!inferredTypes.has(key)) {
|
|
2280
|
+
inferredTypes.set(key, jsToOpenApiType(value));
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const allFields = /* @__PURE__ */ new Set([...inferredTypes.keys(), ...Object.keys(fieldDefs)]);
|
|
2285
|
+
const properties = {};
|
|
2286
|
+
const required = [];
|
|
2287
|
+
for (const field of allFields) {
|
|
2288
|
+
const def = fieldDefs[field];
|
|
2289
|
+
const inferred = inferredTypes.get(field) ?? "string";
|
|
2290
|
+
const prop = {
|
|
2291
|
+
type: def?.type ?? inferred
|
|
2292
|
+
};
|
|
2293
|
+
if (def?.format) prop.format = def.format;
|
|
2294
|
+
if (def?.description) prop.description = def.description;
|
|
2295
|
+
if (def?.enum) prop.enum = def.enum;
|
|
2296
|
+
if (def?.default !== void 0) prop.default = def.default;
|
|
2297
|
+
properties[field] = prop;
|
|
2298
|
+
if (def?.required === true) required.push(field);
|
|
2299
|
+
}
|
|
2300
|
+
const schema = { type: "object", properties };
|
|
2301
|
+
if (required.length > 0) schema.required = required;
|
|
2302
|
+
return schema;
|
|
2303
|
+
}
|
|
2304
|
+
function jsToOpenApiType(value) {
|
|
2305
|
+
if (value === null || value === void 0) return "string";
|
|
2306
|
+
if (typeof value === "boolean") return "boolean";
|
|
2307
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
2308
|
+
if (Array.isArray(value)) return "array";
|
|
2309
|
+
if (typeof value === "object") return "object";
|
|
2310
|
+
return "string";
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// src/openapi/buildPaths.ts
|
|
2314
|
+
init_cjs_shims();
|
|
2315
|
+
var COLLECTION_QUERY_PARAMS = [
|
|
2316
|
+
{ name: "_page", in: "query", schema: { type: "integer" }, description: "Page number (1-based)" },
|
|
2317
|
+
{ name: "_limit", in: "query", schema: { type: "integer" }, description: "Items per page" },
|
|
2318
|
+
{ name: "_sort", in: "query", schema: { type: "string" }, description: "Field name to sort by" },
|
|
2319
|
+
{
|
|
2320
|
+
name: "_order",
|
|
2321
|
+
in: "query",
|
|
2322
|
+
schema: { type: "string", enum: ["asc", "desc"] },
|
|
2323
|
+
description: "Sort direction"
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
name: "_q",
|
|
2327
|
+
in: "query",
|
|
2328
|
+
schema: { type: "string" },
|
|
2329
|
+
description: "Full-text search across all scalar fields (case-insensitive)"
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
name: "_expand",
|
|
2333
|
+
in: "query",
|
|
2334
|
+
schema: { type: "string" },
|
|
2335
|
+
description: "Embed related parent object inline (e.g. ?_expand=user)"
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
name: "_embed",
|
|
2339
|
+
in: "query",
|
|
2340
|
+
schema: { type: "string" },
|
|
2341
|
+
description: "Embed child collection into each item (e.g. ?_embed=posts)"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "_fields",
|
|
2345
|
+
in: "query",
|
|
2346
|
+
schema: { type: "string" },
|
|
2347
|
+
description: "Comma-separated field projection (e.g. ?_fields=id,name)"
|
|
2348
|
+
}
|
|
2349
|
+
];
|
|
2350
|
+
var ID_PATH_PARAM = {
|
|
2351
|
+
name: "id",
|
|
2352
|
+
in: "path",
|
|
2353
|
+
required: true,
|
|
2354
|
+
schema: { type: "string" },
|
|
2355
|
+
description: "Item id"
|
|
2356
|
+
};
|
|
2357
|
+
function toOpenApiPath(fastifyPath) {
|
|
2358
|
+
return fastifyPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "{$1}");
|
|
2359
|
+
}
|
|
2360
|
+
function extractPathParams(fastifyPath) {
|
|
2361
|
+
const matches = fastifyPath.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) ?? [];
|
|
2362
|
+
return matches.map((m) => ({
|
|
2363
|
+
name: m.slice(1),
|
|
2364
|
+
in: "path",
|
|
2365
|
+
required: true,
|
|
2366
|
+
schema: { type: "string" }
|
|
2367
|
+
}));
|
|
2368
|
+
}
|
|
2369
|
+
function singular(name) {
|
|
2370
|
+
return name.endsWith("s") ? name.slice(0, -1) : name;
|
|
2371
|
+
}
|
|
2372
|
+
function schemaRef(name) {
|
|
2373
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
2374
|
+
}
|
|
2375
|
+
function jsonContent(schema) {
|
|
2376
|
+
return { "application/json": { schema } };
|
|
2377
|
+
}
|
|
2378
|
+
function ok(schema, description = "OK") {
|
|
2379
|
+
return { description, content: jsonContent(schema) };
|
|
2380
|
+
}
|
|
2381
|
+
function buildCrudPaths(collection, base, schemaName) {
|
|
2382
|
+
const ref = schemaRef(schemaName);
|
|
2383
|
+
const tag = collection;
|
|
2384
|
+
const sing = singular(collection);
|
|
2385
|
+
const collPath = `${base}/${collection}`;
|
|
2386
|
+
const itemPath = `${base}/${collection}/{id}`;
|
|
2387
|
+
return {
|
|
2388
|
+
[collPath]: {
|
|
2389
|
+
get: {
|
|
2390
|
+
summary: `List ${collection}`,
|
|
2391
|
+
tags: [tag],
|
|
2392
|
+
parameters: COLLECTION_QUERY_PARAMS,
|
|
2393
|
+
responses: {
|
|
2394
|
+
"200": {
|
|
2395
|
+
description: "OK",
|
|
2396
|
+
content: jsonContent({ type: "array", items: ref }),
|
|
2397
|
+
headers: {
|
|
2398
|
+
"X-Total-Count": {
|
|
2399
|
+
description: "Total items (when using ?_page / ?_limit)",
|
|
2400
|
+
schema: { type: "integer" }
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
post: {
|
|
2407
|
+
summary: `Create ${sing}`,
|
|
2408
|
+
tags: [tag],
|
|
2409
|
+
requestBody: { required: true, content: jsonContent(ref) },
|
|
2410
|
+
responses: { "201": ok(ref, "Created") }
|
|
2411
|
+
}
|
|
2412
|
+
},
|
|
2413
|
+
[itemPath]: {
|
|
2414
|
+
get: {
|
|
2415
|
+
summary: `Get ${sing}`,
|
|
2416
|
+
tags: [tag],
|
|
2417
|
+
parameters: [
|
|
2418
|
+
ID_PATH_PARAM,
|
|
2419
|
+
...COLLECTION_QUERY_PARAMS.filter(
|
|
2420
|
+
(p) => ["_expand", "_embed", "_fields"].includes(p.name)
|
|
2421
|
+
)
|
|
2422
|
+
],
|
|
2423
|
+
responses: { "200": ok(ref), "404": { description: "Not found" } }
|
|
2424
|
+
},
|
|
2425
|
+
put: {
|
|
2426
|
+
summary: `Replace ${sing}`,
|
|
2427
|
+
tags: [tag],
|
|
2428
|
+
parameters: [ID_PATH_PARAM],
|
|
2429
|
+
requestBody: { required: true, content: jsonContent(ref) },
|
|
2430
|
+
responses: { "200": ok(ref), "404": { description: "Not found" } }
|
|
2431
|
+
},
|
|
2432
|
+
patch: {
|
|
2433
|
+
summary: `Update ${sing}`,
|
|
2434
|
+
tags: [tag],
|
|
2435
|
+
parameters: [ID_PATH_PARAM],
|
|
2436
|
+
requestBody: { required: false, content: jsonContent(ref) },
|
|
2437
|
+
responses: { "200": ok(ref), "404": { description: "Not found" } }
|
|
2438
|
+
},
|
|
2439
|
+
delete: {
|
|
2440
|
+
summary: `Delete ${sing}`,
|
|
2441
|
+
tags: [tag],
|
|
2442
|
+
parameters: [ID_PATH_PARAM],
|
|
2443
|
+
responses: { "200": ok(ref, "Deleted item returned"), "404": { description: "Not found" } }
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function buildRelationPaths(relations, base) {
|
|
2449
|
+
const paths = {};
|
|
2450
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
2451
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
2452
|
+
if (def.type === "many2many") {
|
|
2453
|
+
const forwardPath = `${base}/${source}/{id}/${key}`;
|
|
2454
|
+
const inversePath = `${base}/${def.target}/{id}/${source}`;
|
|
2455
|
+
paths[forwardPath] = {
|
|
2456
|
+
get: {
|
|
2457
|
+
summary: `List ${def.target} linked to ${singular(source)} via ${def.through}`,
|
|
2458
|
+
tags: [source],
|
|
2459
|
+
parameters: [ID_PATH_PARAM],
|
|
2460
|
+
responses: {
|
|
2461
|
+
"200": {
|
|
2462
|
+
description: "OK",
|
|
2463
|
+
content: jsonContent({ type: "array", items: { type: "object" } })
|
|
2464
|
+
},
|
|
2465
|
+
"404": { description: `${singular(source)} not found` }
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
paths[inversePath] = {
|
|
2470
|
+
get: {
|
|
2471
|
+
summary: `List ${source} linked to ${singular(def.target)} via ${def.through} (inverse)`,
|
|
2472
|
+
tags: [def.target],
|
|
2473
|
+
parameters: [ID_PATH_PARAM],
|
|
2474
|
+
responses: {
|
|
2475
|
+
"200": {
|
|
2476
|
+
description: "OK",
|
|
2477
|
+
content: jsonContent({ type: "array", items: { type: "object" } })
|
|
2478
|
+
},
|
|
2479
|
+
"404": { description: `${singular(def.target)} not found` }
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
} else {
|
|
2484
|
+
const parentSing = singular(def.target);
|
|
2485
|
+
const collPath = `${base}/${def.target}/{id}/${source}`;
|
|
2486
|
+
const isOne2One = def.type === "one2one";
|
|
2487
|
+
const responseSchema = isOne2One ? { type: "object" } : { type: "array", items: { type: "object" } };
|
|
2488
|
+
paths[collPath] = {
|
|
2489
|
+
get: {
|
|
2490
|
+
summary: isOne2One ? `Get ${singular(source)} belonging to ${parentSing}` : `List ${source} belonging to ${parentSing}`,
|
|
2491
|
+
tags: [def.target],
|
|
2492
|
+
parameters: [ID_PATH_PARAM],
|
|
2493
|
+
responses: {
|
|
2494
|
+
"200": { description: "OK", content: jsonContent(responseSchema) },
|
|
2495
|
+
"404": { description: `${parentSing} not found` }
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
if (!isOne2One) {
|
|
2500
|
+
const itemPath = `${base}/${def.target}/{id}/${source}/{childId}`;
|
|
2501
|
+
paths[itemPath] = {
|
|
2502
|
+
get: {
|
|
2503
|
+
summary: `Get single ${singular(source)} scoped to ${parentSing}`,
|
|
2504
|
+
tags: [def.target],
|
|
2505
|
+
parameters: [
|
|
2506
|
+
ID_PATH_PARAM,
|
|
2507
|
+
{ name: "childId", in: "path", required: true, schema: { type: "string" } }
|
|
2508
|
+
],
|
|
2509
|
+
responses: {
|
|
2510
|
+
"200": { description: "OK", content: jsonContent({ type: "object" }) },
|
|
2511
|
+
"404": { description: "Not found" }
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return paths;
|
|
2520
|
+
}
|
|
2521
|
+
function buildCustomRoutePaths(routes, base) {
|
|
2522
|
+
const paths = {};
|
|
2523
|
+
for (const route of routes) {
|
|
2524
|
+
const openApiPath = toOpenApiPath(`${base}${route.path}`);
|
|
2525
|
+
const method = route.method.toLowerCase();
|
|
2526
|
+
const pathParams = extractPathParams(route.path);
|
|
2527
|
+
const responses = {};
|
|
2528
|
+
if (route.error) {
|
|
2529
|
+
responses[String(route.error)] = { description: `Forced error ${route.error}` };
|
|
2530
|
+
} else {
|
|
2531
|
+
const statuses = /* @__PURE__ */ new Set();
|
|
2532
|
+
for (const s of route.scenarios ?? []) statuses.add(s.response.status ?? 200);
|
|
2533
|
+
if (route.otherwise) statuses.add(route.otherwise.status ?? 200);
|
|
2534
|
+
if (route.response) statuses.add(route.response.status ?? 200);
|
|
2535
|
+
if (statuses.size === 0) statuses.add(200);
|
|
2536
|
+
for (const status of statuses) {
|
|
2537
|
+
const bodySource = (route.scenarios ?? []).find((s) => (s.response.status ?? 200) === status)?.response.body ?? (route.otherwise?.status ?? 200) === status ? route.otherwise?.body : route.response?.body;
|
|
2538
|
+
responses[String(status)] = {
|
|
2539
|
+
description: status < 400 ? "OK" : "Error",
|
|
2540
|
+
...bodySource != null ? { content: jsonContent(inferResponseSchema(bodySource)) } : {}
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
const desc = route.handler ? `Handler: ${route.handler}()` : route.scenarios?.length ? `Conditional scenarios (${route.scenarios.length})` : "Custom static route";
|
|
2545
|
+
const operation = {
|
|
2546
|
+
summary: `${route.method.toUpperCase()} ${route.path}`,
|
|
2547
|
+
description: desc,
|
|
2548
|
+
tags: ["custom"],
|
|
2549
|
+
...pathParams.length > 0 ? { parameters: pathParams } : {},
|
|
2550
|
+
responses
|
|
2551
|
+
};
|
|
2552
|
+
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
2553
|
+
paths[openApiPath][method] = operation;
|
|
2554
|
+
}
|
|
2555
|
+
return paths;
|
|
2556
|
+
}
|
|
2557
|
+
function inferResponseSchema(body) {
|
|
2558
|
+
if (body === null || body === void 0) return {};
|
|
2559
|
+
if (typeof body !== "object" || Array.isArray(body)) return { type: "object" };
|
|
2560
|
+
const properties = {};
|
|
2561
|
+
for (const [key, value] of Object.entries(body)) {
|
|
2562
|
+
properties[key] = { type: jsToOpenApiType2(value) };
|
|
2563
|
+
}
|
|
2564
|
+
return { type: "object", properties };
|
|
2565
|
+
}
|
|
2566
|
+
function jsToOpenApiType2(value) {
|
|
2567
|
+
if (value === null || value === void 0) return "string";
|
|
2568
|
+
if (typeof value === "boolean") return "boolean";
|
|
2569
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
2570
|
+
if (Array.isArray(value)) return "array";
|
|
2571
|
+
if (typeof value === "object") return "object";
|
|
2572
|
+
return "string";
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// src/openapi/generateOpenApi.ts
|
|
2576
|
+
function generateOpenApi(storage, options, title = "yRest API") {
|
|
2577
|
+
const collections = Object.keys(storage.getData());
|
|
2578
|
+
const relations = storage.getRelations();
|
|
2579
|
+
const schemaBlock = storage.getSchema();
|
|
2580
|
+
const customRoutes = storage.getRoutes();
|
|
2581
|
+
const base = options.base ?? "";
|
|
2582
|
+
const schemas = {};
|
|
2583
|
+
for (const collection of collections) {
|
|
2584
|
+
const items = storage.getCollection(collection) ?? [];
|
|
2585
|
+
const fieldDefs = schemaBlock[collection] ?? {};
|
|
2586
|
+
const schemaName = toSchemaName(collection);
|
|
2587
|
+
schemas[schemaName] = buildCollectionSchema(items, fieldDefs);
|
|
2588
|
+
}
|
|
2589
|
+
const paths = {};
|
|
2590
|
+
for (const collection of collections) {
|
|
2591
|
+
const schemaName = toSchemaName(collection);
|
|
2592
|
+
Object.assign(paths, buildCrudPaths(collection, base, schemaName));
|
|
2593
|
+
}
|
|
2594
|
+
Object.assign(paths, buildRelationPaths(relations, base));
|
|
2595
|
+
Object.assign(paths, buildCustomRoutePaths(customRoutes, base));
|
|
2596
|
+
return {
|
|
2597
|
+
openapi: "3.0.3",
|
|
2598
|
+
info: {
|
|
2599
|
+
title,
|
|
2600
|
+
version: "1.0.0",
|
|
2601
|
+
description: "Generated by yRest from db.yml"
|
|
2602
|
+
},
|
|
2603
|
+
servers: [
|
|
2604
|
+
{
|
|
2605
|
+
url: `http://${options.host}:${options.port}${base}`,
|
|
2606
|
+
description: "yRest mock server"
|
|
2607
|
+
}
|
|
2608
|
+
],
|
|
2609
|
+
paths,
|
|
2610
|
+
components: { schemas }
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
function toSchemaName(collection) {
|
|
2614
|
+
const withoutS = collection.endsWith("s") ? collection.slice(0, -1) : collection;
|
|
2615
|
+
return withoutS.charAt(0).toUpperCase() + withoutS.slice(1);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// src/router/routes/openapi.routes.ts
|
|
2619
|
+
var import_yaml2 = require("yaml");
|
|
2620
|
+
var OpenApiRouteCommand = class {
|
|
2621
|
+
constructor(storage, options) {
|
|
2622
|
+
this.storage = storage;
|
|
2623
|
+
this.options = options;
|
|
2624
|
+
}
|
|
2625
|
+
storage;
|
|
2626
|
+
options;
|
|
2627
|
+
register(server) {
|
|
2628
|
+
server.get("/_openapi", (_req, reply) => {
|
|
2629
|
+
const doc = generateOpenApi(this.storage, this.options);
|
|
2630
|
+
reply.header("Content-Type", "text/yaml; charset=utf-8");
|
|
2631
|
+
return reply.send((0, import_yaml2.stringify)(doc, { lineWidth: 0, aliasDuplicateObjects: false }));
|
|
2632
|
+
});
|
|
2633
|
+
server.get("/_openapi.json", (_req, reply) => {
|
|
2634
|
+
return reply.send(generateOpenApi(this.storage, this.options));
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
|
|
1514
2639
|
// src/router/routes/snapshot.routes.ts
|
|
1515
2640
|
init_cjs_shims();
|
|
1516
2641
|
var SnapshotRouteCommand = class {
|
|
@@ -1592,6 +2717,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
|
|
|
1592
2717
|
}
|
|
1593
2718
|
const commands = [
|
|
1594
2719
|
new AboutRouteCommand(storage, options, handlers),
|
|
2720
|
+
new OpenApiRouteCommand(storage, options),
|
|
1595
2721
|
...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
|
|
1596
2722
|
new CustomRouteCommand(storage, options.base, handlers),
|
|
1597
2723
|
...buildResourceRouteCommands(storage, options)
|
|
@@ -1671,10 +2797,12 @@ function buildOptions(opts) {
|
|
|
1671
2797
|
}
|
|
1672
2798
|
function createInMemoryStorage(data) {
|
|
1673
2799
|
const raw = data;
|
|
2800
|
+
const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
|
|
1674
2801
|
const relations = parseRelations(raw["_rel"]);
|
|
1675
2802
|
const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
|
|
2803
|
+
const schema = parseSchema(raw["_schema"]);
|
|
1676
2804
|
const collections = Object.fromEntries(
|
|
1677
|
-
Object.entries(raw).filter(([k]) => k
|
|
2805
|
+
Object.entries(raw).filter(([k]) => !RESERVED.has(k))
|
|
1678
2806
|
);
|
|
1679
2807
|
let snapshot = {
|
|
1680
2808
|
data: deepCopyData(collections),
|
|
@@ -1684,6 +2812,7 @@ function createInMemoryStorage(data) {
|
|
|
1684
2812
|
return {
|
|
1685
2813
|
getData: () => collections,
|
|
1686
2814
|
getRelations: () => relations,
|
|
2815
|
+
getSchema: () => schema,
|
|
1687
2816
|
getRoutes: () => routes,
|
|
1688
2817
|
getCollection: (name) => collections[name],
|
|
1689
2818
|
setCollection: (name, items) => {
|