@yrest/cli 0.10.0 → 0.11.1
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 +56 -25
- package/assets/svg/0..1.svg +3 -0
- package/assets/svg/0..n.svg +3 -0
- package/assets/svg/1..1.svg +3 -0
- package/assets/svg/1..n.svg +3 -0
- package/assets/svg/n.svg +3 -0
- 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 +904 -131
- package/dist/cli/index.mjs +904 -131
- package/dist/index.d.mts +8 -10
- package/dist/index.d.ts +8 -10
- package/dist/index.js +864 -81
- package/dist/index.mjs +864 -81
- 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,100 @@ 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
|
+
};
|
|
90
153
|
}
|
|
91
154
|
});
|
|
92
155
|
|
|
@@ -110,13 +173,13 @@ function normaliseFieldDef(value) {
|
|
|
110
173
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
111
174
|
const v = value;
|
|
112
175
|
const def = {};
|
|
113
|
-
if (v["
|
|
114
|
-
if (typeof v["
|
|
115
|
-
def.type = v["
|
|
116
|
-
if (typeof v["
|
|
117
|
-
if (Array.isArray(v["
|
|
118
|
-
if (typeof v["
|
|
119
|
-
if (v["
|
|
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"];
|
|
120
183
|
return def;
|
|
121
184
|
}
|
|
122
185
|
var init_parseSchema = __esm({
|
|
@@ -139,17 +202,125 @@ var init_deepCopy = __esm({
|
|
|
139
202
|
}
|
|
140
203
|
});
|
|
141
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
|
+
|
|
142
259
|
// src/storage/yrestStorage.ts
|
|
143
260
|
var yrestStorage_exports = {};
|
|
144
261
|
__export(yrestStorage_exports, {
|
|
145
262
|
createYrestStorage: () => createYrestStorage
|
|
146
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
|
+
}
|
|
147
318
|
function createYrestStorage(filePath) {
|
|
148
319
|
const absPath = (0, import_node_path2.resolve)(filePath);
|
|
149
320
|
const raw = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
|
|
150
321
|
const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
|
|
151
322
|
const relations = parseRelations(raw["_rel"]);
|
|
152
|
-
const routes =
|
|
323
|
+
const routes = parseRoutes(raw["_routes"]);
|
|
153
324
|
const schema = parseSchema(raw["_schema"]);
|
|
154
325
|
const data = Object.fromEntries(
|
|
155
326
|
Object.entries(raw).filter(([key]) => !RESERVED.has(key))
|
|
@@ -180,8 +351,8 @@ function createYrestStorage(filePath) {
|
|
|
180
351
|
},
|
|
181
352
|
persist() {
|
|
182
353
|
const payload = {};
|
|
183
|
-
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
184
|
-
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);
|
|
185
356
|
Object.assign(payload, data);
|
|
186
357
|
const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto2.randomUUID)()}.tmp`);
|
|
187
358
|
(0, import_node_fs3.writeFileSync)(tmp, (0, import_yaml3.stringify)(payload), "utf8");
|
|
@@ -228,6 +399,7 @@ var init_yrestStorage = __esm({
|
|
|
228
399
|
import_node_crypto2 = require("crypto");
|
|
229
400
|
import_yaml3 = require("yaml");
|
|
230
401
|
init_parseRelations();
|
|
402
|
+
init_parseRoutes();
|
|
231
403
|
init_parseSchema();
|
|
232
404
|
init_deepCopy();
|
|
233
405
|
}
|
|
@@ -680,12 +852,584 @@ curl ${curlFlag}${fullPath}`);
|
|
|
680
852
|
return `<pre>${highlighted}</pre>`;
|
|
681
853
|
}
|
|
682
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
|
+
const pivotFKs = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const fields of Object.values(relations)) {
|
|
872
|
+
for (const def of Object.values(fields)) {
|
|
873
|
+
if (def.type === "many2many") {
|
|
874
|
+
pivotTables.add(def.through);
|
|
875
|
+
if (!pivotFKs.has(def.through)) pivotFKs.set(def.through, /* @__PURE__ */ new Set());
|
|
876
|
+
pivotFKs.get(def.through).add(def.foreignKey);
|
|
877
|
+
pivotFKs.get(def.through).add(def.otherKey);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const fkFields = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
883
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
884
|
+
if (def.type !== "many2many") {
|
|
885
|
+
if (!fkFields.has(source)) fkFields.set(source, /* @__PURE__ */ new Set());
|
|
886
|
+
fkFields.get(source).add(key);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const entities = Object.entries(data).map(([name, items]) => {
|
|
891
|
+
const sample = items[0] ?? {};
|
|
892
|
+
const schemaFields = schema[name] ?? {};
|
|
893
|
+
const allFields = /* @__PURE__ */ new Set([...Object.keys(sample), ...Object.keys(schemaFields)]);
|
|
894
|
+
const fields = [...allFields].map((fieldName) => ({
|
|
895
|
+
name: fieldName,
|
|
896
|
+
type: schemaFields[fieldName]?.type ?? inferType(sample[fieldName]),
|
|
897
|
+
pk: fieldName === "id",
|
|
898
|
+
fk: fkFields.get(name)?.has(fieldName) ?? false,
|
|
899
|
+
required: schemaFields[fieldName]?.required ?? false
|
|
900
|
+
}));
|
|
901
|
+
fields.sort((a, b) => {
|
|
902
|
+
if (a.pk) return -1;
|
|
903
|
+
if (b.pk) return 1;
|
|
904
|
+
if (a.fk && !b.fk) return 1;
|
|
905
|
+
if (!a.fk && b.fk) return -1;
|
|
906
|
+
return 0;
|
|
907
|
+
});
|
|
908
|
+
return { name, fields, isPivot: pivotTables.has(name) };
|
|
909
|
+
});
|
|
910
|
+
const erRelations = [];
|
|
911
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
912
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
913
|
+
if (def.type === "many2many") {
|
|
914
|
+
const throughRels = relations[def.through] ?? {};
|
|
915
|
+
const fkRel = throughRels[def.foreignKey];
|
|
916
|
+
const otherRel = throughRels[def.otherKey];
|
|
917
|
+
erRelations.push({
|
|
918
|
+
source,
|
|
919
|
+
target: def.target,
|
|
920
|
+
type: "many2many",
|
|
921
|
+
label: def.through,
|
|
922
|
+
through: def.through,
|
|
923
|
+
foreignKey: def.foreignKey,
|
|
924
|
+
otherKey: def.otherKey,
|
|
925
|
+
fkCarDirect: fkRel?.carDirect,
|
|
926
|
+
fkCarInverse: fkRel?.carInverse,
|
|
927
|
+
otherCarDirect: otherRel?.carDirect,
|
|
928
|
+
otherCarInverse: otherRel?.carInverse
|
|
929
|
+
});
|
|
930
|
+
} else {
|
|
931
|
+
if (pivotFKs.get(source)?.has(key)) continue;
|
|
932
|
+
erRelations.push({
|
|
933
|
+
source,
|
|
934
|
+
target: def.target,
|
|
935
|
+
type: def.type,
|
|
936
|
+
label: key,
|
|
937
|
+
carDirect: def.carDirect,
|
|
938
|
+
carInverse: def.carInverse
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return { entities, relations: erRelations };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/router/templates/about.diagram.canvas.ts
|
|
947
|
+
init_cjs_shims();
|
|
948
|
+
function erDiagramSection(data) {
|
|
949
|
+
if (!data.entities.length) return "";
|
|
950
|
+
const json = JSON.stringify(data);
|
|
951
|
+
return (
|
|
952
|
+
/*html*/
|
|
953
|
+
`
|
|
954
|
+
<h2>Data Model</h2>
|
|
955
|
+
|
|
956
|
+
<div class="card" style="padding:0;overflow:hidden;position:relative;">
|
|
957
|
+
<canvas id="er-canvas" style="width:100%;display:block;cursor:grab;"></canvas>
|
|
958
|
+
<div style="position:absolute;top:10px;right:14px;display:flex;gap:6px;align-items:center;">
|
|
959
|
+
<select id="er-scale" title="Download quality" style="background:#161b22;border:1px solid #30363d;color:#7d8590;padding:3px 6px;border-radius:6px;font-size:11px;cursor:pointer;">
|
|
960
|
+
<option value="1">1\xD7</option>
|
|
961
|
+
<option value="2">2\xD7</option>
|
|
962
|
+
<option value="3" selected>3\xD7</option>
|
|
963
|
+
<option value="4">4\xD7</option>
|
|
964
|
+
</select>
|
|
965
|
+
<button id="er-dl" title="Download PNG" style="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>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
<script>${/*js*/
|
|
970
|
+
`
|
|
971
|
+
(function(){
|
|
972
|
+
|
|
973
|
+
var D=${json};
|
|
974
|
+
|
|
975
|
+
// \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
|
|
976
|
+
var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=12,BO=16,LGH=84,MIN_GAP=10;
|
|
977
|
+
|
|
978
|
+
var C={bg:'#0d1117',nb:'#161b22',nh:'#1c2128',pb:'#0d1117',ph:'#161b22',
|
|
979
|
+
bo:'#30363d',bs:'#58a6ff',tx:'#e6edf3',mu:'#7d8590',
|
|
980
|
+
pk:'#e3b341',fk:'#79c0ff',m2o:'#58a6ff',o2o:'#34d399',m2m:'#818cf8'};
|
|
981
|
+
|
|
982
|
+
// \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
|
|
983
|
+
var cv = document.getElementById('er-canvas');
|
|
984
|
+
var dpr = window.devicePixelRatio||1;
|
|
985
|
+
var ctx = cv.getContext('2d');
|
|
986
|
+
|
|
987
|
+
function resize(h){
|
|
988
|
+
if(h)cv.style.height = h + 'px';
|
|
989
|
+
var r = cv.getBoundingClientRect();
|
|
990
|
+
cv.width = Math.round(r.width*dpr);
|
|
991
|
+
cv.height = Math.round(r.height*dpr);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// \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
|
|
995
|
+
var nodes = D.entities.map(function(e){
|
|
996
|
+
return{e:e,x:0,y:0,w:NW,h:HH+e.fields.length*FH+6};
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
var nIdx={};D.entities.forEach(function(e,i){nIdx[e.name]=i;});
|
|
1000
|
+
|
|
1001
|
+
var adj=nodes.map(function(){return[];}); // forward
|
|
1002
|
+
var radj=nodes.map(function(){return[];}); // reverse
|
|
1003
|
+
|
|
1004
|
+
D.relations.forEach(function(r){
|
|
1005
|
+
if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
|
|
1006
|
+
var s=nIdx[r.source],p=nIdx[r.through],t=nIdx[r.target];
|
|
1007
|
+
if(s!=null&&p!=null&&s!==p){adj[s].push(p);radj[p].push(s);}
|
|
1008
|
+
if(t!=null&&p!=null&&t!==p){adj[t].push(p);radj[p].push(t);}
|
|
1009
|
+
}else{
|
|
1010
|
+
var s=nIdx[r.source],t=nIdx[r.target];
|
|
1011
|
+
if(s==null||t==null||s===t)return;
|
|
1012
|
+
adj[s].push(t);radj[t].push(s);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// \u2500\u2500 Dynamic node width \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1017
|
+
function computeNodeWidths(){
|
|
1018
|
+
nodes.forEach(function(n){
|
|
1019
|
+
var e=n.e;
|
|
1020
|
+
ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
|
|
1021
|
+
var hdrW=FP+ctx.measureText(e.name).width+FP;
|
|
1022
|
+
if(e.isPivot){
|
|
1023
|
+
ctx.font='9px monospace';
|
|
1024
|
+
hdrW+=ctx.measureText('pivot').width+FP;
|
|
1025
|
+
}
|
|
1026
|
+
var maxRow=hdrW;
|
|
1027
|
+
e.fields.forEach(function(f){
|
|
1028
|
+
var leftPad=FP+(f.pk||f.fk?16:0);
|
|
1029
|
+
var rightPad=(f.required&&!f.pk?14:0)+FP;
|
|
1030
|
+
ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
|
|
1031
|
+
var nw=ctx.measureText(f.name).width;
|
|
1032
|
+
ctx.font='10px monospace';
|
|
1033
|
+
var tw=ctx.measureText(f.type).width;
|
|
1034
|
+
maxRow=Math.max(maxRow,leftPad+nw+MIN_GAP+tw+rightPad);
|
|
1035
|
+
});
|
|
1036
|
+
n.w=Math.max(NW,Math.ceil(maxRow));
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// \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
|
|
1041
|
+
var groups=[];
|
|
1042
|
+
|
|
1043
|
+
function median(arr){
|
|
1044
|
+
if(!arr.length)return 0;
|
|
1045
|
+
var s=[].concat(arr).sort(function(a,b){return a-b;});
|
|
1046
|
+
return s[Math.floor(s.length/2)];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function computeLayout(W,H){
|
|
1050
|
+
var layer=nodes.map(function(){return 0;});
|
|
1051
|
+
var inDeg=nodes.map(function(){return 0;});
|
|
1052
|
+
adj.forEach(function(ts){ts.forEach(function(t){inDeg[t]++;});});
|
|
1053
|
+
var q=[],qi=0;
|
|
1054
|
+
for(var i=0;i<nodes.length;i++)if(inDeg[i]===0)q.push(i);
|
|
1055
|
+
if(!q.length)for(var i=0;i<nodes.length;i++)q.push(i);
|
|
1056
|
+
while(qi<q.length){
|
|
1057
|
+
var u=q[qi++];
|
|
1058
|
+
adj[u].forEach(function(v){if(layer[v]<=layer[u]){layer[v]=layer[u]+1;q.push(v);}});
|
|
1059
|
+
}
|
|
1060
|
+
var maxL=layer.reduce(function(m,l){return Math.max(m,l);},0);
|
|
1061
|
+
groups=[];
|
|
1062
|
+
for(var l=0;l<=maxL;l++)groups.push([]);
|
|
1063
|
+
for(var i=0;i<nodes.length;i++)groups[layer[i]].push(i);
|
|
1064
|
+
for(var l=1;l<=maxL;l++){
|
|
1065
|
+
var pp={};groups[l-1].forEach(function(i,p){pp[i]=p;});
|
|
1066
|
+
groups[l].sort(function(a,b){
|
|
1067
|
+
return median(radj[a].map(function(x){return pp[x]!=null?pp[x]:0;}))-
|
|
1068
|
+
median(radj[b].map(function(x){return pp[x]!=null?pp[x]:0;}));
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
for(var l=maxL-1;l>=0;l--){
|
|
1072
|
+
var np={};groups[l+1].forEach(function(i,p){np[i]=p;});
|
|
1073
|
+
groups[l].sort(function(a,b){
|
|
1074
|
+
return median(adj[a].map(function(x){return np[x]!=null?np[x]:0;}))-
|
|
1075
|
+
median(adj[b].map(function(x){return np[x]!=null?np[x]:0;}));
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
var maxNW=nodes.reduce(function(m,n){return Math.max(m,n.w);},NW);
|
|
1079
|
+
var colW=maxNW+COL_GAP;
|
|
1080
|
+
var totalW=(maxL+1)*colW-COL_GAP;
|
|
1081
|
+
var startX=Math.max(20,(W-totalW)/2);
|
|
1082
|
+
for(var l=0;l<=maxL;l++){
|
|
1083
|
+
var g=groups[l];
|
|
1084
|
+
var totalH=g.reduce(function(s,i){return s+nodes[i].h;},0)+(g.length-1)*ROW_GAP;
|
|
1085
|
+
var y=Math.max(20,(H-totalH)/2);
|
|
1086
|
+
g.forEach(function(i){nodes[i].x=startX+l*colW;nodes[i].y=y;y+=nodes[i].h+ROW_GAP;});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function requiredH(){
|
|
1091
|
+
return Math.max(340,Math.min(720,groups.reduce(function(m,g){
|
|
1092
|
+
return Math.max(m,g.reduce(function(s,i){return s+nodes[i].h;},0)+(g.length-1)*ROW_GAP+LGH+70);
|
|
1093
|
+
},300)));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// \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
|
|
1097
|
+
var pan={x:0,y:0},zm=1;
|
|
1098
|
+
var origPos=[];
|
|
1099
|
+
|
|
1100
|
+
function savePos(){origPos=nodes.map(function(n){return{x:n.x,y:n.y};});}
|
|
1101
|
+
|
|
1102
|
+
function fitView(W,H){
|
|
1103
|
+
var x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;
|
|
1104
|
+
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);});
|
|
1105
|
+
var pad=28,gW=x1-x0+pad*2,gH=y1-y0+pad*2;
|
|
1106
|
+
zm=Math.min(W/gW,(H-LGH)/gH,1);
|
|
1107
|
+
pan.x=(W-gW*zm)/2+pad*zm-x0*zm;
|
|
1108
|
+
pan.y=((H-LGH)-gH*zm)/2+pad*zm-y0*zm;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// \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
|
|
1112
|
+
function rr(x,y,w,h,r){
|
|
1113
|
+
if(ctx.roundRect){ctx.beginPath();ctx.roundRect(x,y,w,h,r);return;}
|
|
1114
|
+
ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);
|
|
1115
|
+
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();
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function edgePt(n,tx,ty){
|
|
1119
|
+
var cx=n.x+n.w/2,cy=n.y+n.h/2,dx=tx-cx,dy=ty-cy;
|
|
1120
|
+
if(!dx&&!dy)return{x:cx,y:cy};
|
|
1121
|
+
var s=Math.min((n.w/2)/Math.abs(dx||.001),(n.h/2)/Math.abs(dy||.001));
|
|
1122
|
+
return{x:cx+dx*s,y:cy+dy*s};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
var badges=[];
|
|
1126
|
+
|
|
1127
|
+
// \u2500\u2500 SVG cardinality badges via Path2D \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1128
|
+
// SVG orientation: LEFT side touches the node, RIGHT side connects to the line.
|
|
1129
|
+
// Rotation = Math.atan2(uy,ux) aligns local +x with the outward (away-from-node) direction.
|
|
1130
|
+
var P2D={
|
|
1131
|
+
'1..1':new Path2D('M75 40C75 59.33 59.33 75 40 75C20.67 75 5 59.33 5 40C5 20.67 20.67 5 40 5C59.33 5 75 20.67 75 40ZM41 26C41 24.3431 39.6569 23 38 23C36.3431 23 35 24.3431 35 26V37H26V26C26 24.3431 24.6569 23 23 23C21.3431 23 20 24.3431 20 26V37H15C13.3431 37 12 38.3431 12 40C12 41.6569 13.3431 43 15 43H20V54C20 55.6569 21.3431 57 23 57C24.6569 57 26 55.6569 26 54V43H35V54C35 55.6569 36.3431 57 38 57C39.6569 57 41 55.6569 41 54V43H66C67.6569 43 69 41.6569 69 40C69 38.3431 67.6569 37 66 37H41V26Z'),
|
|
1132
|
+
'1..n':new Path2D('M40 75C59.33 75 75 59.33 75 40C75 20.67 59.33 5 40 5C20.67 5 5 20.67 5 40C5 59.33 20.67 75 40 75ZM44.9862 42.9025L18.6801 58.0904C17.2452 58.9188 15.4105 58.4272 14.582 56.9923C13.7536 55.5574 14.2452 53.7227 15.6801 52.8942L32.9862 42.9025H17C15.3431 42.9025 14 41.5594 14 39.9025C14 38.2457 15.3431 36.9025 17 36.9025H32.9862L15.5981 26.5986C14.1632 25.7702 13.6716 23.9354 14.5 22.5005C15.3284 21.0656 17.1632 20.574 18.5981 21.4024L44.9862 36.9025H46V26C46 24.3431 47.3431 23 49 23C50.6569 23 52 24.3431 52 26V36.9025H68C69.6569 36.9025 71 38.2457 71 39.9025C71 41.5594 69.6569 42.9025 68 42.9025H52V53C52 54.6569 50.6569 56 49 56C47.3431 56 46 54.6569 46 53V42.9025H44.9862Z'),
|
|
1133
|
+
'0..1':new Path2D('M75 40C75 59.33 59.33 75 40 75C20.67 75 5 59.33 5 40C5 20.67 20.67 5 40 5C59.33 5 75 20.67 75 40ZM23 57C24.6569 57 26 55.6569 26 54V43H33.3781C34.7102 48.1757 39.4085 52 45 52C50.5915 52 55.2898 48.1757 56.6219 43H66C67.6569 43 69 41.6569 69 40C69 38.3431 67.6569 37 66 37H56.6219C55.2898 31.8243 50.5915 28 45 28C39.4085 28 34.7102 31.8243 33.3781 37H26V26C26 24.3431 24.6569 23 23 23C21.3431 23 20 24.3431 20 26V37H15C13.3431 37 12 38.3431 12 40C12 41.6569 13.3431 43 15 43H20V54C20 55.6569 21.3431 57 23 57ZM51 40C51 41.0929 50.7078 42.1175 50.1973 43C49.1599 44.7934 47.2208 46 45 46C42.7792 46 40.8401 44.7934 39.8027 43C39.2922 42.1175 39 41.0929 39 40C39 38.9071 39.2922 37.8825 39.8027 37C40.8401 35.2066 42.7792 34 45 34C47.2208 34 49.1599 35.2066 50.1973 37C50.7078 37.8825 51 38.9071 51 40Z'),
|
|
1134
|
+
'0..n':new Path2D('M40 75C59.33 75 75 59.33 75 40C75 20.67 59.33 5 40 5C20.67 5 5 20.67 5 40C5 59.33 20.67 75 40 75ZM17 42.9025C15.3431 42.9025 14 41.5594 14 39.9025C14 38.2457 15.3431 36.9025 17 36.9025H32.9862L15.5981 26.5986C14.1632 25.7702 13.6716 23.9354 14.5 22.5005C15.3284 21.0656 17.1632 20.574 18.5981 21.4024L45.4091 36.8818L45.4036 36.9025H45.445L45.4091 36.8818C46.7819 31.7662 51.4511 28 57 28C62.579 28 67.2687 31.8072 68.6129 36.9652C69.9758 37.2481 71 38.4557 71 39.9025C71 41.331 70.0017 42.5262 68.6647 42.8286C67.3931 48.091 62.6531 52 57 52C51.3734 52 46.6513 48.1276 45.3534 42.9025H44.9862L18.6801 58.0904C17.2452 58.9188 15.4105 58.4272 14.582 56.9923C13.7536 55.5574 14.2452 53.7227 15.6801 52.8942L32.9862 42.9025H17ZM62.1397 36.9025C61.0891 35.163 59.1804 34 57 34C54.8196 34 52.9109 35.163 51.8603 36.9025C51.3142 37.8067 51 38.8667 51 40C51 41.0529 51.2712 42.0424 51.7475 42.9025C52.7704 44.7496 54.7392 46 57 46C59.2608 46 61.2296 44.7496 62.2525 42.9025C62.7288 42.0424 63 41.0529 63 40C63 38.8667 62.6858 37.8067 62.1397 36.9025Z'),
|
|
1135
|
+
'n':new Path2D('M40 75C59.33 75 75 59.33 75 40C75 20.67 59.33 5 40 5C20.67 5 5 20.67 5 40C5 59.33 20.67 75 40 75ZM17 36.9025H33.0677L15.5981 26.5986C14.1632 25.7702 13.6716 23.9354 14.5 22.5005C15.3284 21.0656 17.1632 20.574 18.5981 21.4024L44.9862 36.9025H68C69.6569 36.9025 71 38.2457 71 39.9025C71 41.5594 69.6569 42.9025 68 42.9025H44.9862L18.6801 58.0904C17.2452 58.9188 15.4105 58.4272 14.582 56.9923C13.7536 55.5574 14.2452 53.7227 15.6801 52.8942L32.9862 42.9025H17C15.3431 42.9025 14 41.5594 14 39.9025C14 38.2457 15.3431 36.9025 17 36.9025Z')
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
function pushBadge(x,y,ux,uy,car,col){
|
|
1139
|
+
badges.push({x:x,y:y,ux:ux,uy:uy,car:car,col:col});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function drawBadge(b){
|
|
1143
|
+
var p=P2D[b.car]||P2D['n'],sc=BR/40;
|
|
1144
|
+
ctx.save();
|
|
1145
|
+
ctx.translate(b.x+b.ux*BO,b.y+b.uy*BO);
|
|
1146
|
+
ctx.rotate(Math.atan2(b.uy,b.ux));
|
|
1147
|
+
ctx.scale(sc,sc);
|
|
1148
|
+
ctx.translate(-40,-40);
|
|
1149
|
+
ctx.fillStyle=b.col;
|
|
1150
|
+
ctx.fill(p,'evenodd');
|
|
1151
|
+
ctx.restore();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function ecol(t){return t==='one2one'?C.o2o:t==='many2many'?C.m2m:C.m2o;}
|
|
1155
|
+
|
|
1156
|
+
// offset: pixels to shift perpendicular to the edge direction (for parallel edges)
|
|
1157
|
+
function drawEdgeLine(a,b,col,dashed,label,carA,carB,off){
|
|
1158
|
+
var p1=edgePt(a,b.x+b.w/2,b.y+b.h/2);
|
|
1159
|
+
var p2=edgePt(b,a.x+a.w/2,a.y+a.h/2);
|
|
1160
|
+
var dx=p2.x-p1.x,dy=p2.y-p1.y,d=Math.sqrt(dx*dx+dy*dy)||1;
|
|
1161
|
+
var ux=dx/d,uy=dy/d;
|
|
1162
|
+
var px=-uy*off,py=ux*off; // perpendicular shift
|
|
1163
|
+
var q1x=p1.x+px,q1y=p1.y+py,q2x=p2.x+px,q2y=p2.y+py;
|
|
1164
|
+
ctx.strokeStyle=col+'55';ctx.lineWidth=1.5;
|
|
1165
|
+
ctx.setLineDash(dashed?[5,4]:[]);
|
|
1166
|
+
ctx.beginPath();ctx.moveTo(q1x,q1y);ctx.lineTo(q2x,q2y);ctx.stroke();
|
|
1167
|
+
ctx.setLineDash([]);
|
|
1168
|
+
pushBadge(q1x,q1y,ux,uy,carA,col);
|
|
1169
|
+
pushBadge(q2x,q2y,-ux,-uy,carB,col);
|
|
1170
|
+
if(label){
|
|
1171
|
+
var mx=(q1x+q2x)/2,my=(q1y+q2y)/2;
|
|
1172
|
+
var la=Math.atan2(uy,ux);
|
|
1173
|
+
if(la>Math.PI/2||la<-Math.PI/2)la+=Math.PI; // keep text readable
|
|
1174
|
+
var ls=off>0?11:-11; // alternate above/below for parallel edges
|
|
1175
|
+
ctx.save();
|
|
1176
|
+
ctx.translate(mx,my);
|
|
1177
|
+
ctx.rotate(la);
|
|
1178
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
|
|
1179
|
+
ctx.fillText(label,0,ls);
|
|
1180
|
+
ctx.restore();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// canonical key for a node pair regardless of direction
|
|
1185
|
+
function pairKey(a,b){
|
|
1186
|
+
var ai=nodes.indexOf(a),bi=nodes.indexOf(b);
|
|
1187
|
+
return ai<bi?ai+'-'+bi:bi+'-'+ai;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function collectEdges(){
|
|
1191
|
+
var edges=[];
|
|
1192
|
+
D.relations.forEach(function(r){
|
|
1193
|
+
var col=ecol(r.type);
|
|
1194
|
+
if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
|
|
1195
|
+
var sn=nodes[nIdx[r.source]],pn=nodes[nIdx[r.through]],tn=nodes[nIdx[r.target]];
|
|
1196
|
+
if(sn&&pn)edges.push({a:sn,b:pn,col:col,dashed:true,label:r.foreignKey||'',carA:r.fkCarDirect||'1..1',carB:r.fkCarInverse||'0..n',off:0});
|
|
1197
|
+
if(tn&&pn)edges.push({a:tn,b:pn,col:col,dashed:true,label:r.otherKey||'',carA:r.otherCarDirect||'1..1',carB:r.otherCarInverse||'0..n',off:0});
|
|
1198
|
+
}else{
|
|
1199
|
+
var sn=nodes[nIdx[r.source]],tn=nodes[nIdx[r.target]];
|
|
1200
|
+
if(sn&&tn&&sn!==tn){
|
|
1201
|
+
var carA=r.carInverse||(r.type!=='one2one'?'1..n':'1..1');
|
|
1202
|
+
var carB=r.carDirect||(r.type==='many2many'?'1..n':'1..1');
|
|
1203
|
+
edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,carA:carA,carB:carB,off:0});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
// assign perpendicular offsets to parallel edges (same node pair)
|
|
1208
|
+
var cnt={},idx={};
|
|
1209
|
+
edges.forEach(function(e){var k=pairKey(e.a,e.b);cnt[k]=(cnt[k]||0)+1;});
|
|
1210
|
+
edges.forEach(function(e){
|
|
1211
|
+
var k=pairKey(e.a,e.b),n=cnt[k];
|
|
1212
|
+
var i=idx[k]=(idx[k]||0);
|
|
1213
|
+
e.off=n>1?(i-(n-1)/2)*16:0;
|
|
1214
|
+
idx[k]++;
|
|
1215
|
+
});
|
|
1216
|
+
return edges;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function drawNode(n){
|
|
1220
|
+
var x=n.x,y=n.y,w=n.w,h=n.h,e=n.e;
|
|
1221
|
+
var bg=e.isPivot?C.pb:C.nb,hd=e.isPivot?C.ph:C.nh;
|
|
1222
|
+
ctx.save();ctx.shadowColor='rgba(0,0,0,0.45)';ctx.shadowBlur=14;ctx.shadowOffsetY=4;
|
|
1223
|
+
ctx.fillStyle=bg;rr(x,y,w,h,7);ctx.fill();ctx.restore();
|
|
1224
|
+
ctx.fillStyle=bg;rr(x,y,w,h,7);ctx.fill();
|
|
1225
|
+
ctx.strokeStyle=n===drag?C.bs:C.bo;ctx.lineWidth=1.2;rr(x,y,w,h,7);ctx.stroke();
|
|
1226
|
+
ctx.fillStyle=hd;rr(x,y,w,HH,7);ctx.fill();ctx.fillRect(x,y+HH-7,w,7);
|
|
1227
|
+
ctx.strokeStyle=C.bo;ctx.lineWidth=1;
|
|
1228
|
+
ctx.beginPath();ctx.moveTo(x,y+HH);ctx.lineTo(x+w,y+HH);ctx.stroke();
|
|
1229
|
+
ctx.fillStyle=e.isPivot?C.mu:C.tx;
|
|
1230
|
+
ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
|
|
1231
|
+
ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1232
|
+
ctx.fillText(e.name,x+FP,y+HH/2);
|
|
1233
|
+
if(e.isPivot){
|
|
1234
|
+
ctx.fillStyle='#3d444d';ctx.font='9px monospace';ctx.textAlign='right';
|
|
1235
|
+
ctx.fillText('pivot',x+w-FP,y+HH/2);
|
|
1236
|
+
}
|
|
1237
|
+
e.fields.forEach(function(f,fi){
|
|
1238
|
+
var fy=y+HH+fi*FH+FH/2+3;
|
|
1239
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
|
|
1240
|
+
ctx.fillText(f.type,x+w-FP-(f.required&&!f.pk?14:0),fy);
|
|
1241
|
+
if(f.pk||f.fk){
|
|
1242
|
+
var tag=f.pk?'PK':'FK',tc=f.pk?C.pk:C.fk;
|
|
1243
|
+
ctx.fillStyle=tc+'22';ctx.strokeStyle=tc+'66';ctx.lineWidth=1;
|
|
1244
|
+
rr(x+FP,fy-6,12,12,2);ctx.fill();ctx.stroke();
|
|
1245
|
+
ctx.fillStyle=tc;ctx.font='7px monospace';ctx.textAlign='center';
|
|
1246
|
+
ctx.fillText(tag,x+FP+6,fy+0.5);
|
|
1247
|
+
}
|
|
1248
|
+
ctx.fillStyle=f.pk?C.pk:f.fk?C.fk:C.tx;
|
|
1249
|
+
ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
|
|
1250
|
+
ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1251
|
+
ctx.fillText(f.name,x+FP+(f.pk||f.fk?16:0),fy);
|
|
1252
|
+
if(f.required&&!f.pk){
|
|
1253
|
+
ctx.fillStyle='#f85149';ctx.beginPath();
|
|
1254
|
+
ctx.arc(x+w-FP-6,fy,2.5,0,Math.PI*2);ctx.fill();
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
ctx.textBaseline='alphabetic';
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function drawLegend(){
|
|
1261
|
+
var cvW=cv.width/dpr,cvH=cv.height/dpr;
|
|
1262
|
+
var yL=cvH-LGH,y1=yL+13,y2=yL+35,y3=yL+62;
|
|
1263
|
+
|
|
1264
|
+
ctx.fillStyle='#161b22';
|
|
1265
|
+
ctx.fillRect(0,yL,cvW,LGH);
|
|
1266
|
+
ctx.strokeStyle='#30363d';ctx.lineWidth=1;
|
|
1267
|
+
ctx.beginPath();ctx.moveTo(0,yL);ctx.lineTo(cvW,yL);ctx.stroke();
|
|
1268
|
+
|
|
1269
|
+
function legendBadge(cx,cy,car,col,angle){
|
|
1270
|
+
var p=P2D[car]||P2D['n'],sc=8/40;
|
|
1271
|
+
ctx.save();
|
|
1272
|
+
ctx.translate(cx,cy);
|
|
1273
|
+
ctx.rotate(angle);
|
|
1274
|
+
ctx.scale(sc,sc);
|
|
1275
|
+
ctx.translate(-40,-40);
|
|
1276
|
+
ctx.fillStyle=col;
|
|
1277
|
+
ctx.fill(p,'evenodd');
|
|
1278
|
+
ctx.restore();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Row 1: relation line types
|
|
1282
|
+
var types=[
|
|
1283
|
+
{c:C.m2o,l:'many2one',d:false},
|
|
1284
|
+
{c:C.o2o,l:'one2one',d:false},
|
|
1285
|
+
{c:C.m2m,l:'many2many',d:true}
|
|
1286
|
+
];
|
|
1287
|
+
ctx.textAlign='left';ctx.textBaseline='middle';ctx.font='10px monospace';
|
|
1288
|
+
var x=14;
|
|
1289
|
+
types.forEach(function(it){
|
|
1290
|
+
ctx.strokeStyle=it.c;ctx.lineWidth=1.5;ctx.setLineDash(it.d?[4,3]:[]);
|
|
1291
|
+
ctx.beginPath();ctx.moveTo(x,y1);ctx.lineTo(x+14,y1);ctx.stroke();
|
|
1292
|
+
ctx.setLineDash([]);
|
|
1293
|
+
ctx.fillStyle=C.mu;
|
|
1294
|
+
ctx.fillText(it.l,x+18,y1);
|
|
1295
|
+
x+=18+ctx.measureText(it.l).width+22;
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// Row 2: relation type examples (pair of badges + line)
|
|
1299
|
+
x=14;
|
|
1300
|
+
var pairs=[
|
|
1301
|
+
{carA:'1..n',carB:'1..1',col:C.m2o,l:'many2one (N:1)'},
|
|
1302
|
+
{carA:'1..1',carB:'1..1',col:C.o2o,l:'one2one (1:1)'},
|
|
1303
|
+
{carA:'1..1',carB:'0..n',col:C.m2m,l:'pivot edge (1:0..N)'}
|
|
1304
|
+
];
|
|
1305
|
+
pairs.forEach(function(p){
|
|
1306
|
+
ctx.strokeStyle=p.col+'55';ctx.lineWidth=1.2;ctx.setLineDash(p.col===C.m2m?[4,3]:[]);
|
|
1307
|
+
ctx.beginPath();ctx.moveTo(x+8,y2);ctx.lineTo(x+56,y2);ctx.stroke();
|
|
1308
|
+
ctx.setLineDash([]);
|
|
1309
|
+
legendBadge(x+8,y2,p.carA,p.col,0);
|
|
1310
|
+
legendBadge(x+56,y2,p.carB,p.col,Math.PI);
|
|
1311
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1312
|
+
ctx.fillText(p.l,x+66,y2);
|
|
1313
|
+
x+=66+ctx.measureText(p.l).width+24;
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
// Row 3: individual symbol meanings
|
|
1317
|
+
var symbols=[
|
|
1318
|
+
{car:'1..1',l:'exactly one'},
|
|
1319
|
+
{car:'0..1',l:'zero or one'},
|
|
1320
|
+
{car:'1..n',l:'one or more'},
|
|
1321
|
+
{car:'0..n',l:'zero or more'},
|
|
1322
|
+
{car:'n', l:'many (pivot)'}
|
|
1323
|
+
];
|
|
1324
|
+
x=14;
|
|
1325
|
+
ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1326
|
+
symbols.forEach(function(s){
|
|
1327
|
+
legendBadge(x+8,y3,s.car,C.mu,0);
|
|
1328
|
+
ctx.fillStyle=C.mu;
|
|
1329
|
+
ctx.fillText(s.l,x+20,y3);
|
|
1330
|
+
x+=20+ctx.measureText(s.l).width+18;
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
ctx.fillStyle='#3d444d';ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
|
|
1334
|
+
ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y1);
|
|
1335
|
+
ctx.textBaseline='alphabetic';
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function draw(){
|
|
1339
|
+
var W=cv.width/dpr,H=cv.height/dpr;
|
|
1340
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
1341
|
+
ctx.clearRect(0,0,W,H);
|
|
1342
|
+
ctx.fillStyle=C.bg;ctx.fillRect(0,0,W,H);
|
|
1343
|
+
ctx.save();ctx.translate(pan.x,pan.y);ctx.scale(zm,zm);
|
|
1344
|
+
badges=[];
|
|
1345
|
+
var edges=collectEdges();
|
|
1346
|
+
edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.carA,e.carB,e.off);});
|
|
1347
|
+
nodes.forEach(function(n){drawNode(n);});
|
|
1348
|
+
badges.forEach(function(b){drawBadge(b);});
|
|
1349
|
+
ctx.restore();
|
|
1350
|
+
drawLegend();
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// \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
|
|
1354
|
+
var drag=null,dox=0,doy=0,isPan=false,ps={x:0,y:0};
|
|
1355
|
+
|
|
1356
|
+
function toW(mx,my){return{x:(mx-pan.x)/zm,y:(my-pan.y)/zm};}
|
|
1357
|
+
function hitTest(wx,wy){
|
|
1358
|
+
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;}
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
cv.addEventListener('mousedown',function(ev){
|
|
1363
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1364
|
+
var w=toW(mx,my),h=hitTest(w.x,w.y);
|
|
1365
|
+
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};}
|
|
1366
|
+
cv.style.cursor='grabbing';
|
|
1367
|
+
});
|
|
1368
|
+
window.addEventListener('mousemove',function(ev){
|
|
1369
|
+
if(!drag&&!isPan)return;
|
|
1370
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1371
|
+
if(drag){var w=toW(mx,my);drag.x=w.x-dox;drag.y=w.y-doy;}
|
|
1372
|
+
else{pan.x=mx-ps.x;pan.y=my-ps.y;}
|
|
1373
|
+
draw();
|
|
1374
|
+
});
|
|
1375
|
+
window.addEventListener('mouseup',function(){drag=null;isPan=false;cv.style.cursor='grab';});
|
|
1376
|
+
cv.addEventListener('wheel',function(ev){
|
|
1377
|
+
ev.preventDefault();
|
|
1378
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1379
|
+
var d=ev.deltaY<0?1.1:0.91,nz=Math.max(0.25,Math.min(3,zm*d));
|
|
1380
|
+
pan.x=mx-(mx-pan.x)*(nz/zm);pan.y=my-(my-pan.y)*(nz/zm);zm=nz;draw();
|
|
1381
|
+
},{passive:false});
|
|
1382
|
+
cv.addEventListener('dblclick',function(){
|
|
1383
|
+
nodes.forEach(function(n,i){n.x=origPos[i].x;n.y=origPos[i].y;});
|
|
1384
|
+
var r=cv.getBoundingClientRect();fitView(r.width,r.height);draw();
|
|
1385
|
+
});
|
|
1386
|
+
window.addEventListener('resize',function(){
|
|
1387
|
+
resize();var r=cv.getBoundingClientRect();
|
|
1388
|
+
computeLayout(r.width,r.height);savePos();fitView(r.width,r.height);draw();
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
document.getElementById('er-dl').addEventListener('click',function(){
|
|
1392
|
+
var SCALE=parseInt(document.getElementById('er-scale').value,10)||3;
|
|
1393
|
+
var pad=36;
|
|
1394
|
+
var bx0=1e9,by0=1e9,bx1=-1e9,by1=-1e9;
|
|
1395
|
+
nodes.forEach(function(n){
|
|
1396
|
+
bx0=Math.min(bx0,n.x);by0=Math.min(by0,n.y);
|
|
1397
|
+
bx1=Math.max(bx1,n.x+n.w);by1=Math.max(by1,n.y+n.h);
|
|
1398
|
+
});
|
|
1399
|
+
var cw=bx1-bx0+pad*2,ch=by1-by0+pad*2+LGH;
|
|
1400
|
+
var oc=document.createElement('canvas');
|
|
1401
|
+
oc.width=Math.round(cw*SCALE);oc.height=Math.round(ch*SCALE);
|
|
1402
|
+
var sav={cv:cv,ctx:ctx,dpr:dpr,px:pan.x,py:pan.y,zm:zm};
|
|
1403
|
+
cv=oc;ctx=oc.getContext('2d');dpr=SCALE;
|
|
1404
|
+
pan.x=pad-bx0;pan.y=pad-by0;zm=1;
|
|
1405
|
+
draw();
|
|
1406
|
+
cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;pan.x=sav.px;pan.y=sav.py;zm=sav.zm;
|
|
1407
|
+
var a=document.createElement('a');
|
|
1408
|
+
a.download='er-diagram.png';a.href=oc.toDataURL('image/png');a.click();
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// \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
|
|
1412
|
+
requestAnimationFrame(function(){
|
|
1413
|
+
resize();
|
|
1414
|
+
computeNodeWidths();
|
|
1415
|
+
var r=cv.getBoundingClientRect();
|
|
1416
|
+
computeLayout(r.width,r.height);
|
|
1417
|
+
var h=requiredH();
|
|
1418
|
+
resize(h);r=cv.getBoundingClientRect();
|
|
1419
|
+
computeLayout(r.width,r.height);
|
|
1420
|
+
savePos();fitView(r.width,r.height);draw();
|
|
1421
|
+
});
|
|
1422
|
+
})();
|
|
1423
|
+
`}</script>`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
683
1427
|
// src/router/templates/about.template.ts
|
|
684
1428
|
var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
685
1429
|
var LOGO_SRC = (() => {
|
|
686
1430
|
try {
|
|
687
|
-
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/
|
|
688
|
-
return `data:image/
|
|
1431
|
+
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/yrest-lockup.svg"));
|
|
1432
|
+
return `data:image/svg+xml;base64,${buf.toString("base64")}`;
|
|
689
1433
|
} catch {
|
|
690
1434
|
return "";
|
|
691
1435
|
}
|
|
@@ -696,6 +1440,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
696
1440
|
const customRoutes = storage.getRoutes();
|
|
697
1441
|
const base = options.base;
|
|
698
1442
|
const host = `http://${options.host}:${options.port}`;
|
|
1443
|
+
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
699
1444
|
const modes = [];
|
|
700
1445
|
if (options.watch) modes.push(badge("watch", "#38bdf8", "#38bdf818"));
|
|
701
1446
|
if (options.readonly) modes.push(badge("readonly", "#94a3b8", "#94a3b818"));
|
|
@@ -706,9 +1451,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
706
1451
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
707
1452
|
if (options.idStrategy !== "increment")
|
|
708
1453
|
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
1454
|
+
const erDiagram = erDiagramSection(generateERData(storage));
|
|
709
1455
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
710
1456
|
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.`;
|
|
711
|
-
return
|
|
1457
|
+
return (
|
|
1458
|
+
/*html*/
|
|
1459
|
+
`<!DOCTYPE html>
|
|
712
1460
|
<html lang="en">
|
|
713
1461
|
<head>
|
|
714
1462
|
<meta charset="UTF-8">
|
|
@@ -735,20 +1483,30 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
735
1483
|
width: 100%;
|
|
736
1484
|
background: linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a2332 100%);
|
|
737
1485
|
border-bottom: 1px solid var(--border);
|
|
738
|
-
padding:
|
|
1486
|
+
padding: 20px 32px 16px;
|
|
739
1487
|
}
|
|
740
|
-
.banner-inner {
|
|
741
|
-
.banner
|
|
1488
|
+
.banner-inner { margin: 0 auto; display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; }
|
|
1489
|
+
.banner-left { flex:1; min-width:0; }
|
|
1490
|
+
.banner h1 { font-size: clamp(24px, 4vw, 40px); font-weight: 800; letter-spacing: -1.5px; line-height: 1; }
|
|
742
1491
|
.banner h1 .y { color: var(--text); }
|
|
743
1492
|
.banner h1 .rest { color: var(--accent); }
|
|
744
|
-
.banner p { color: var(--text-muted); margin-top:
|
|
745
|
-
.banner-meta { display: flex; gap:
|
|
746
|
-
.banner-meta span { color: var(--text-muted); font-size:
|
|
1493
|
+
.banner p { color: var(--text-muted); margin-top: 6px; font-size: 14px; }
|
|
1494
|
+
.banner-meta { display: flex; gap: 20px; margin-top: 10px; flex-wrap: wrap; }
|
|
1495
|
+
.banner-meta span { color: var(--text-muted); font-size: 12px; }
|
|
747
1496
|
.banner-meta span strong { color: var(--text); font-family: monospace; }
|
|
1497
|
+
.api-links { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
|
1498
|
+
.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; }
|
|
1499
|
+
.btn-api:hover { border-color:var(--border-hi); color:var(--text); }
|
|
1500
|
+
.btn-swagger { border-color:var(--accent); color:var(--accent); }
|
|
1501
|
+
.btn-swagger:hover { background:#58a6ff18; }
|
|
748
1502
|
|
|
749
1503
|
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
750
|
-
.wrap {
|
|
1504
|
+
.wrap { padding: 24px 32px 48px; }
|
|
751
1505
|
h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin: 32px 0 12px; }
|
|
1506
|
+
.about-grid { display:grid; grid-template-columns:5fr 7fr; gap:28px; align-items:start; }
|
|
1507
|
+
@media(max-width:960px) { .about-grid { grid-template-columns:1fr; } }
|
|
1508
|
+
.col-left, .col-right { min-width:0; }
|
|
1509
|
+
.col-left > h2:first-child, .col-right > h2:first-child { margin-top:0; }
|
|
752
1510
|
|
|
753
1511
|
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
754
1512
|
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; }
|
|
@@ -764,10 +1522,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
764
1522
|
.modes { display: flex; gap: 8px; flex-wrap: wrap; min-height: 28px; align-items: center; }
|
|
765
1523
|
.badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
766
1524
|
|
|
767
|
-
/* \u2500\u2500 Endpoints
|
|
768
|
-
.endpoints-grid { display:
|
|
769
|
-
@media (max-width: 860px) { .endpoints-grid { grid-template-columns: 1fr; } }
|
|
770
|
-
.nested-card { grid-column: 1 / -1; }
|
|
1525
|
+
/* \u2500\u2500 Endpoints list \u2500\u2500 */
|
|
1526
|
+
.endpoints-grid { display: flex; flex-direction: column; gap: 10px; }
|
|
771
1527
|
|
|
772
1528
|
/* \u2500\u2500 Accordion (details/summary) \u2500\u2500 */
|
|
773
1529
|
.resource-card {
|
|
@@ -832,64 +1588,80 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
832
1588
|
|
|
833
1589
|
<div class="banner">
|
|
834
1590
|
<div class="banner-inner">
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1591
|
+
<div class="banner-left">
|
|
1592
|
+
${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>`}
|
|
1593
|
+
<p>Zero-config REST API mock server</p>
|
|
1594
|
+
<div class="banner-meta">
|
|
1595
|
+
<span>URL <strong>${host}</strong></span>
|
|
1596
|
+
<span>Base <strong>${base || "/"}</strong></span>
|
|
1597
|
+
<span>File <strong>${options.file}</strong></span>
|
|
1598
|
+
<span>Collections <strong>${collections.length}</strong></span>
|
|
1599
|
+
</div>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div class="api-links">
|
|
1602
|
+
<a href="/_openapi" target="_blank" class="btn-api">OpenAPI YAML</a>
|
|
1603
|
+
<a href="/_openapi.json" target="_blank" class="btn-api">OpenAPI JSON</a>
|
|
1604
|
+
<a href="${swaggerUrl}" target="_blank" class="btn-api btn-swagger">Swagger UI \u2197</a>
|
|
842
1605
|
</div>
|
|
843
1606
|
</div>
|
|
844
1607
|
</div>
|
|
845
1608
|
|
|
846
1609
|
<div class="wrap">
|
|
1610
|
+
<div class="about-grid">
|
|
1611
|
+
|
|
1612
|
+
<div class="col-left">
|
|
1613
|
+
<h2>Active Modes</h2>
|
|
1614
|
+
<div class="card">
|
|
1615
|
+
<div class="modes">${modes.length ? modes.join(" ") : `<span style="color:var(--text-muted);font-size:13px">none</span>`}</div>
|
|
1616
|
+
${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>` : ""}
|
|
1617
|
+
</div>
|
|
1618
|
+
|
|
1619
|
+
<h2>Endpoints</h2>
|
|
1620
|
+
<div class="endpoints-grid">
|
|
1621
|
+
${accordions}
|
|
1622
|
+
${nestedRoutesAccordion(relations, base)}
|
|
1623
|
+
${options.snapshot ? snapshotAccordion() : ""}
|
|
1624
|
+
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
1625
|
+
${handlersAccordion(handlers, customRoutes, base)}
|
|
1626
|
+
</div>
|
|
1627
|
+
|
|
1628
|
+
<h2>Query Parameters</h2>
|
|
1629
|
+
<div class="card">
|
|
1630
|
+
<table class="param-table">
|
|
1631
|
+
<thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
|
|
1632
|
+
<tbody>
|
|
1633
|
+
<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>
|
|
1634
|
+
<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>
|
|
1635
|
+
<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>
|
|
1636
|
+
<tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
|
|
1637
|
+
<tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
|
|
1638
|
+
<tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
|
|
1639
|
+
<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>
|
|
1640
|
+
<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>
|
|
1641
|
+
<tr><td><code>?_page & ?_limit</code></td><td><code>?_page=2&_limit=10</code></td><td>${paginationDesc}</td></tr>
|
|
1642
|
+
<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>
|
|
1643
|
+
<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>
|
|
1644
|
+
<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>
|
|
1645
|
+
</tbody>
|
|
1646
|
+
</table>
|
|
1647
|
+
</div>
|
|
1648
|
+
</div>
|
|
847
1649
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
</div>
|
|
853
|
-
|
|
854
|
-
<h2>Endpoints</h2>
|
|
855
|
-
<div class="endpoints-grid">
|
|
856
|
-
${accordions}
|
|
857
|
-
${nestedRoutesAccordion(relations, base)}
|
|
858
|
-
${options.snapshot ? snapshotAccordion() : ""}
|
|
859
|
-
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
860
|
-
${handlersAccordion(handlers, customRoutes, base)}
|
|
861
|
-
</div>
|
|
1650
|
+
<div class="col-right">
|
|
1651
|
+
${erDiagram}
|
|
1652
|
+
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
1653
|
+
</div>
|
|
862
1654
|
|
|
863
|
-
<h2>Query Parameters</h2>
|
|
864
|
-
<div class="card">
|
|
865
|
-
<table class="param-table">
|
|
866
|
-
<thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
|
|
867
|
-
<tbody>
|
|
868
|
-
<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>
|
|
869
|
-
<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>
|
|
870
|
-
<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>
|
|
871
|
-
<tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
|
|
872
|
-
<tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
|
|
873
|
-
<tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
|
|
874
|
-
<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>
|
|
875
|
-
<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>
|
|
876
|
-
<tr><td><code>?_page & ?_limit</code></td><td><code>?_page=2&_limit=10</code></td><td>${paginationDesc}</td></tr>
|
|
877
|
-
<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>
|
|
878
|
-
<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>
|
|
879
|
-
<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>
|
|
880
|
-
</tbody>
|
|
881
|
-
</table>
|
|
882
1655
|
</div>
|
|
883
1656
|
|
|
884
|
-
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
885
|
-
|
|
886
1657
|
<footer>
|
|
887
1658
|
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
888
1659
|
</footer>
|
|
889
1660
|
|
|
890
1661
|
</div>
|
|
891
1662
|
</body>
|
|
892
|
-
</html
|
|
1663
|
+
</html>`
|
|
1664
|
+
);
|
|
893
1665
|
}
|
|
894
1666
|
|
|
895
1667
|
// src/router/routes/about.routes.ts
|
|
@@ -1490,12 +2262,23 @@ var NestedRouteCommand = class {
|
|
|
1490
2262
|
relations;
|
|
1491
2263
|
base;
|
|
1492
2264
|
register(server) {
|
|
2265
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1493
2266
|
for (const [source, fields] of Object.entries(this.relations)) {
|
|
1494
2267
|
for (const [key, def] of Object.entries(fields)) {
|
|
1495
2268
|
if (def.type === "many2many") {
|
|
1496
|
-
this.
|
|
2269
|
+
const fwd = `${this.base}/${source}/:id/${key}`;
|
|
2270
|
+
const inv = `${this.base}/${def.target}/:id/${source}`;
|
|
2271
|
+
if (!registered.has(fwd) && !registered.has(inv)) {
|
|
2272
|
+
registered.add(fwd);
|
|
2273
|
+
registered.add(inv);
|
|
2274
|
+
this.registerMany2Many(server, source, key, def);
|
|
2275
|
+
}
|
|
1497
2276
|
} else {
|
|
1498
|
-
|
|
2277
|
+
const collPath = `${this.base}/${def.target}/:id/${source}`;
|
|
2278
|
+
if (!registered.has(collPath)) {
|
|
2279
|
+
registered.add(collPath);
|
|
2280
|
+
this.registerFkRelation(server, source, key, def.target, def.type);
|
|
2281
|
+
}
|
|
1499
2282
|
}
|
|
1500
2283
|
}
|
|
1501
2284
|
}
|