@yrest/cli 0.10.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 +56 -25
- 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 +830 -131
- package/dist/cli/index.mjs +830 -131
- package/dist/index.d.mts +8 -10
- package/dist/index.d.ts +8 -10
- package/dist/index.js +790 -81
- package/dist/index.mjs +790 -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,510 @@ 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
|
+
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
|
+
|
|
683
1353
|
// src/router/templates/about.template.ts
|
|
684
1354
|
var _dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
685
1355
|
var LOGO_SRC = (() => {
|
|
686
1356
|
try {
|
|
687
|
-
const buf = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(_dir, "../../assets/
|
|
688
|
-
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")}`;
|
|
689
1359
|
} catch {
|
|
690
1360
|
return "";
|
|
691
1361
|
}
|
|
@@ -696,6 +1366,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
696
1366
|
const customRoutes = storage.getRoutes();
|
|
697
1367
|
const base = options.base;
|
|
698
1368
|
const host = `http://${options.host}:${options.port}`;
|
|
1369
|
+
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
699
1370
|
const modes = [];
|
|
700
1371
|
if (options.watch) modes.push(badge("watch", "#38bdf8", "#38bdf818"));
|
|
701
1372
|
if (options.readonly) modes.push(badge("readonly", "#94a3b8", "#94a3b818"));
|
|
@@ -706,9 +1377,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
706
1377
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
707
1378
|
if (options.idStrategy !== "increment")
|
|
708
1379
|
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
1380
|
+
const erDiagram = erDiagramSection(generateERData(storage));
|
|
709
1381
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
710
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.`;
|
|
711
|
-
return
|
|
1383
|
+
return (
|
|
1384
|
+
/*html*/
|
|
1385
|
+
`<!DOCTYPE html>
|
|
712
1386
|
<html lang="en">
|
|
713
1387
|
<head>
|
|
714
1388
|
<meta charset="UTF-8">
|
|
@@ -735,20 +1409,30 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
735
1409
|
width: 100%;
|
|
736
1410
|
background: linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a2332 100%);
|
|
737
1411
|
border-bottom: 1px solid var(--border);
|
|
738
|
-
padding:
|
|
1412
|
+
padding: 20px 32px 16px;
|
|
739
1413
|
}
|
|
740
|
-
.banner-inner {
|
|
741
|
-
.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; }
|
|
742
1417
|
.banner h1 .y { color: var(--text); }
|
|
743
1418
|
.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:
|
|
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; }
|
|
747
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; }
|
|
748
1428
|
|
|
749
1429
|
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
750
|
-
.wrap {
|
|
1430
|
+
.wrap { padding: 24px 32px 48px; }
|
|
751
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; }
|
|
752
1436
|
|
|
753
1437
|
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
754
1438
|
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; }
|
|
@@ -764,10 +1448,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
764
1448
|
.modes { display: flex; gap: 8px; flex-wrap: wrap; min-height: 28px; align-items: center; }
|
|
765
1449
|
.badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
766
1450
|
|
|
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; }
|
|
1451
|
+
/* \u2500\u2500 Endpoints list \u2500\u2500 */
|
|
1452
|
+
.endpoints-grid { display: flex; flex-direction: column; gap: 10px; }
|
|
771
1453
|
|
|
772
1454
|
/* \u2500\u2500 Accordion (details/summary) \u2500\u2500 */
|
|
773
1455
|
.resource-card {
|
|
@@ -832,64 +1514,80 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
832
1514
|
|
|
833
1515
|
<div class="banner">
|
|
834
1516
|
<div class="banner-inner">
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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>
|
|
842
1531
|
</div>
|
|
843
1532
|
</div>
|
|
844
1533
|
</div>
|
|
845
1534
|
|
|
846
1535
|
<div class="wrap">
|
|
1536
|
+
<div class="about-grid">
|
|
1537
|
+
|
|
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>
|
|
1544
|
+
|
|
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>
|
|
1553
|
+
|
|
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>
|
|
847
1575
|
|
|
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>
|
|
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>
|
|
862
1580
|
|
|
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
1581
|
</div>
|
|
883
1582
|
|
|
884
|
-
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
885
|
-
|
|
886
1583
|
<footer>
|
|
887
1584
|
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
888
1585
|
</footer>
|
|
889
1586
|
|
|
890
1587
|
</div>
|
|
891
1588
|
</body>
|
|
892
|
-
</html
|
|
1589
|
+
</html>`
|
|
1590
|
+
);
|
|
893
1591
|
}
|
|
894
1592
|
|
|
895
1593
|
// src/router/routes/about.routes.ts
|
|
@@ -1490,12 +2188,23 @@ var NestedRouteCommand = class {
|
|
|
1490
2188
|
relations;
|
|
1491
2189
|
base;
|
|
1492
2190
|
register(server) {
|
|
2191
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1493
2192
|
for (const [source, fields] of Object.entries(this.relations)) {
|
|
1494
2193
|
for (const [key, def] of Object.entries(fields)) {
|
|
1495
2194
|
if (def.type === "many2many") {
|
|
1496
|
-
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
|
+
}
|
|
1497
2202
|
} else {
|
|
1498
|
-
|
|
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
|
+
}
|
|
1499
2208
|
}
|
|
1500
2209
|
}
|
|
1501
2210
|
}
|