@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.mjs
CHANGED
|
@@ -33,37 +33,100 @@ function parseRelations(raw) {
|
|
|
33
33
|
}
|
|
34
34
|
function normaliseRelationDef(key, value) {
|
|
35
35
|
if (typeof value === "string") {
|
|
36
|
-
return { type: "many2one", target: value };
|
|
36
|
+
return value.includes(":") ? parseDslString(value) : { type: "many2one", target: value };
|
|
37
37
|
}
|
|
38
38
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
39
39
|
const v = value;
|
|
40
|
-
const type = v["
|
|
41
|
-
const nested = v["
|
|
40
|
+
const type = v["_type"];
|
|
41
|
+
const nested = v["_nested"] === true ? true : void 0;
|
|
42
|
+
const carDirect = typeof v["_car-direct"] === "string" ? v["_car-direct"] : void 0;
|
|
43
|
+
const carInverse = typeof v["_car-inverse"] === "string" ? v["_car-inverse"] : void 0;
|
|
42
44
|
if (type === "many2one" || type === void 0) {
|
|
43
|
-
const target = v["
|
|
45
|
+
const target = v["_target"];
|
|
44
46
|
if (typeof target !== "string") return null;
|
|
45
|
-
|
|
47
|
+
const foreignKey = typeof v["_foreignKey"] === "string" ? v["_foreignKey"] : void 0;
|
|
48
|
+
return compact({ type: "many2one", target, foreignKey, nested, carDirect, carInverse });
|
|
46
49
|
}
|
|
47
50
|
if (type === "one2one") {
|
|
48
|
-
const target = v["
|
|
51
|
+
const target = v["_target"];
|
|
49
52
|
if (typeof target !== "string") return null;
|
|
50
|
-
|
|
53
|
+
const foreignKey = typeof v["_foreignKey"] === "string" ? v["_foreignKey"] : void 0;
|
|
54
|
+
return compact({ type: "one2one", target, foreignKey, nested, carDirect, carInverse });
|
|
51
55
|
}
|
|
52
56
|
if (type === "many2many") {
|
|
53
|
-
const target = typeof v["
|
|
54
|
-
const through = v["
|
|
55
|
-
const foreignKey = v["
|
|
56
|
-
const otherKey = v["
|
|
57
|
+
const target = typeof v["_target"] === "string" ? v["_target"] : key;
|
|
58
|
+
const through = v["_through"];
|
|
59
|
+
const foreignKey = v["_foreignKey"];
|
|
60
|
+
const otherKey = v["_otherKey"];
|
|
57
61
|
if (typeof through !== "string" || typeof foreignKey !== "string" || typeof otherKey !== "string")
|
|
58
62
|
return null;
|
|
59
|
-
return
|
|
63
|
+
return compact({
|
|
64
|
+
type: "many2many",
|
|
65
|
+
target,
|
|
66
|
+
through,
|
|
67
|
+
foreignKey,
|
|
68
|
+
otherKey,
|
|
69
|
+
nested,
|
|
70
|
+
carDirect,
|
|
71
|
+
carInverse
|
|
72
|
+
});
|
|
60
73
|
}
|
|
61
74
|
return null;
|
|
62
75
|
}
|
|
76
|
+
function parseDslString(raw) {
|
|
77
|
+
const nested = raw.endsWith("+nested") ? true : void 0;
|
|
78
|
+
let s = nested ? raw.slice(0, -7) : raw;
|
|
79
|
+
let carDirect;
|
|
80
|
+
let carInverse;
|
|
81
|
+
const carMatch = s.match(/\[([^[\]]+)->([^[\]]+)\]$/);
|
|
82
|
+
if (carMatch) {
|
|
83
|
+
carDirect = carMatch[1];
|
|
84
|
+
carInverse = carMatch[2];
|
|
85
|
+
s = s.slice(0, s.length - carMatch[0].length);
|
|
86
|
+
}
|
|
87
|
+
const colonIdx = s.indexOf(":");
|
|
88
|
+
if (colonIdx === -1) return null;
|
|
89
|
+
const typeAlias = s.slice(0, colonIdx);
|
|
90
|
+
const body = s.slice(colonIdx + 1);
|
|
91
|
+
const type = TYPE_ALIASES[typeAlias];
|
|
92
|
+
if (!type) return null;
|
|
93
|
+
if (type === "many2many") {
|
|
94
|
+
const m = body.match(/^([^@()[\]]+)@([^@()[\]]+)\(([^,)]+),\s*([^,)]+)\)$/);
|
|
95
|
+
if (!m) return null;
|
|
96
|
+
const [, target2, through, foreignKey2, otherKey] = m;
|
|
97
|
+
return compact({
|
|
98
|
+
type: "many2many",
|
|
99
|
+
target: target2,
|
|
100
|
+
through,
|
|
101
|
+
foreignKey: foreignKey2,
|
|
102
|
+
otherKey,
|
|
103
|
+
nested,
|
|
104
|
+
carDirect,
|
|
105
|
+
carInverse
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const atIdx = body.indexOf("@");
|
|
109
|
+
const target = atIdx === -1 ? body : body.slice(0, atIdx);
|
|
110
|
+
const foreignKey = atIdx === -1 ? void 0 : body.slice(atIdx + 1);
|
|
111
|
+
if (!target) return null;
|
|
112
|
+
return compact({ type, target, foreignKey, nested, carDirect, carInverse });
|
|
113
|
+
}
|
|
114
|
+
function compact(obj) {
|
|
115
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
116
|
+
}
|
|
117
|
+
var TYPE_ALIASES;
|
|
63
118
|
var init_parseRelations = __esm({
|
|
64
119
|
"src/storage/parseRelations.ts"() {
|
|
65
120
|
"use strict";
|
|
66
121
|
init_esm_shims();
|
|
122
|
+
TYPE_ALIASES = {
|
|
123
|
+
many2one: "many2one",
|
|
124
|
+
m2o: "many2one",
|
|
125
|
+
one2one: "one2one",
|
|
126
|
+
o2o: "one2one",
|
|
127
|
+
many2many: "many2many",
|
|
128
|
+
m2m: "many2many"
|
|
129
|
+
};
|
|
67
130
|
}
|
|
68
131
|
});
|
|
69
132
|
|
|
@@ -87,13 +150,13 @@ function normaliseFieldDef(value) {
|
|
|
87
150
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
88
151
|
const v = value;
|
|
89
152
|
const def = {};
|
|
90
|
-
if (v["
|
|
91
|
-
if (typeof v["
|
|
92
|
-
def.type = v["
|
|
93
|
-
if (typeof v["
|
|
94
|
-
if (Array.isArray(v["
|
|
95
|
-
if (typeof v["
|
|
96
|
-
if (v["
|
|
153
|
+
if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
|
|
154
|
+
if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
|
|
155
|
+
def.type = v["_type"];
|
|
156
|
+
if (typeof v["_format"] === "string") def.format = v["_format"];
|
|
157
|
+
if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
|
|
158
|
+
if (typeof v["_description"] === "string") def.description = v["_description"];
|
|
159
|
+
if (v["_default"] !== void 0) def.default = v["_default"];
|
|
97
160
|
return def;
|
|
98
161
|
}
|
|
99
162
|
var init_parseSchema = __esm({
|
|
@@ -116,6 +179,60 @@ var init_deepCopy = __esm({
|
|
|
116
179
|
}
|
|
117
180
|
});
|
|
118
181
|
|
|
182
|
+
// src/storage/parseRoutes.ts
|
|
183
|
+
function parseRoutes(raw) {
|
|
184
|
+
if (!Array.isArray(raw)) return [];
|
|
185
|
+
return raw.map(normaliseRoute).filter((r) => r !== null);
|
|
186
|
+
}
|
|
187
|
+
function normaliseRoute(raw) {
|
|
188
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
189
|
+
const v = raw;
|
|
190
|
+
const method = v["_method"];
|
|
191
|
+
const path2 = v["_path"];
|
|
192
|
+
if (typeof method !== "string" || typeof path2 !== "string") return null;
|
|
193
|
+
const route = { method, path: path2 };
|
|
194
|
+
if (typeof v["_handler"] === "string") route.handler = v["_handler"];
|
|
195
|
+
if (typeof v["_delay"] === "number") route.delay = v["_delay"];
|
|
196
|
+
if (typeof v["_error"] === "number") route.error = v["_error"];
|
|
197
|
+
if (v["_errorBody"] !== void 0) route.errorBody = v["_errorBody"];
|
|
198
|
+
const response = normaliseResponse(v["_response"]);
|
|
199
|
+
if (response) route.response = response;
|
|
200
|
+
const otherwise = normaliseResponse(v["_otherwise"]);
|
|
201
|
+
if (otherwise) route.otherwise = otherwise;
|
|
202
|
+
const scenarios = normaliseScenarios(v["_scenarios"]);
|
|
203
|
+
if (scenarios.length > 0) route.scenarios = scenarios;
|
|
204
|
+
return route;
|
|
205
|
+
}
|
|
206
|
+
function normaliseResponse(raw) {
|
|
207
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
208
|
+
const v = raw;
|
|
209
|
+
const response = {};
|
|
210
|
+
if (typeof v["_status"] === "number") response.status = v["_status"];
|
|
211
|
+
if (v["_body"] !== void 0) response.body = v["_body"];
|
|
212
|
+
if (v["_headers"] && typeof v["_headers"] === "object" && !Array.isArray(v["_headers"]))
|
|
213
|
+
response.headers = v["_headers"];
|
|
214
|
+
return Object.keys(response).length > 0 ? response : void 0;
|
|
215
|
+
}
|
|
216
|
+
function normaliseScenarios(raw) {
|
|
217
|
+
if (!Array.isArray(raw)) return [];
|
|
218
|
+
return raw.map(normaliseScenario).filter((s) => s !== null);
|
|
219
|
+
}
|
|
220
|
+
function normaliseScenario(raw) {
|
|
221
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
222
|
+
const v = raw;
|
|
223
|
+
const when = v["_when"];
|
|
224
|
+
if (!when || typeof when !== "object") return null;
|
|
225
|
+
const response = normaliseResponse(v["_response"]);
|
|
226
|
+
if (!response) return null;
|
|
227
|
+
return { when, response };
|
|
228
|
+
}
|
|
229
|
+
var init_parseRoutes = __esm({
|
|
230
|
+
"src/storage/parseRoutes.ts"() {
|
|
231
|
+
"use strict";
|
|
232
|
+
init_esm_shims();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
119
236
|
// src/storage/yrestStorage.ts
|
|
120
237
|
var yrestStorage_exports = {};
|
|
121
238
|
__export(yrestStorage_exports, {
|
|
@@ -125,12 +242,66 @@ import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
|
125
242
|
import { resolve, dirname as dirname2 } from "path";
|
|
126
243
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
127
244
|
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
245
|
+
function serializeRelations(relations) {
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const [col, fields] of Object.entries(relations)) {
|
|
248
|
+
out[col] = {};
|
|
249
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
250
|
+
out[col][key] = serializeRelationDef(def);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
function serializeRelationDef(def) {
|
|
256
|
+
if (def.type === "many2many") {
|
|
257
|
+
const entry2 = {
|
|
258
|
+
_type: def.type,
|
|
259
|
+
_target: def.target,
|
|
260
|
+
_through: def.through,
|
|
261
|
+
_foreignKey: def.foreignKey,
|
|
262
|
+
_otherKey: def.otherKey
|
|
263
|
+
};
|
|
264
|
+
if (def.nested) entry2._nested = true;
|
|
265
|
+
if (def.carDirect) entry2["_car-direct"] = def.carDirect;
|
|
266
|
+
if (def.carInverse) entry2["_car-inverse"] = def.carInverse;
|
|
267
|
+
return entry2;
|
|
268
|
+
}
|
|
269
|
+
const entry = { _type: def.type, _target: def.target };
|
|
270
|
+
if (def.foreignKey) entry._foreignKey = def.foreignKey;
|
|
271
|
+
if (def.nested) entry._nested = true;
|
|
272
|
+
if (def.carDirect) entry["_car-direct"] = def.carDirect;
|
|
273
|
+
if (def.carInverse) entry["_car-inverse"] = def.carInverse;
|
|
274
|
+
return entry;
|
|
275
|
+
}
|
|
276
|
+
function serializeRoutes(routes) {
|
|
277
|
+
return routes.map((r) => {
|
|
278
|
+
const entry = { _method: r.method, _path: r.path };
|
|
279
|
+
if (r.handler) entry._handler = r.handler;
|
|
280
|
+
if (r.delay !== void 0) entry._delay = r.delay;
|
|
281
|
+
if (r.error !== void 0) entry._error = r.error;
|
|
282
|
+
if (r.errorBody !== void 0) entry._errorBody = r.errorBody;
|
|
283
|
+
if (r.response) entry._response = serializeResponse(r.response);
|
|
284
|
+
if (r.otherwise) entry._otherwise = serializeResponse(r.otherwise);
|
|
285
|
+
if (r.scenarios?.length) entry._scenarios = r.scenarios.map(serializeScenario);
|
|
286
|
+
return entry;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function serializeResponse(res) {
|
|
290
|
+
const out = {};
|
|
291
|
+
if (res.status !== void 0) out._status = res.status;
|
|
292
|
+
if (res.body !== void 0) out._body = res.body;
|
|
293
|
+
if (res.headers) out._headers = res.headers;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
function serializeScenario(s) {
|
|
297
|
+
return { _when: s.when, _response: serializeResponse(s.response) };
|
|
298
|
+
}
|
|
128
299
|
function createYrestStorage(filePath) {
|
|
129
300
|
const absPath = resolve(filePath);
|
|
130
301
|
const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
|
|
131
302
|
const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
|
|
132
303
|
const relations = parseRelations(raw["_rel"]);
|
|
133
|
-
const routes =
|
|
304
|
+
const routes = parseRoutes(raw["_routes"]);
|
|
134
305
|
const schema = parseSchema(raw["_schema"]);
|
|
135
306
|
const data = Object.fromEntries(
|
|
136
307
|
Object.entries(raw).filter(([key]) => !RESERVED.has(key))
|
|
@@ -161,8 +332,8 @@ function createYrestStorage(filePath) {
|
|
|
161
332
|
},
|
|
162
333
|
persist() {
|
|
163
334
|
const payload = {};
|
|
164
|
-
if (Object.keys(relations).length > 0) payload._rel = relations;
|
|
165
|
-
if (routes.length > 0) payload._routes = routes;
|
|
335
|
+
if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
|
|
336
|
+
if (routes.length > 0) payload._routes = serializeRoutes(routes);
|
|
166
337
|
Object.assign(payload, data);
|
|
167
338
|
const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
|
|
168
339
|
writeFileSync(tmp, stringify2(payload), "utf8");
|
|
@@ -204,6 +375,7 @@ var init_yrestStorage = __esm({
|
|
|
204
375
|
"use strict";
|
|
205
376
|
init_esm_shims();
|
|
206
377
|
init_parseRelations();
|
|
378
|
+
init_parseRoutes();
|
|
207
379
|
init_parseSchema();
|
|
208
380
|
init_deepCopy();
|
|
209
381
|
}
|
|
@@ -646,12 +818,510 @@ curl ${curlFlag}${fullPath}`);
|
|
|
646
818
|
return `<pre>${highlighted}</pre>`;
|
|
647
819
|
}
|
|
648
820
|
|
|
821
|
+
// src/router/templates/about.diagram.ts
|
|
822
|
+
init_esm_shims();
|
|
823
|
+
function inferType(value) {
|
|
824
|
+
if (value === null || value === void 0) return "string";
|
|
825
|
+
if (typeof value === "boolean") return "boolean";
|
|
826
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
827
|
+
if (Array.isArray(value)) return "array";
|
|
828
|
+
if (typeof value === "object") return "object";
|
|
829
|
+
return "string";
|
|
830
|
+
}
|
|
831
|
+
function generateERData(storage) {
|
|
832
|
+
const data = storage.getData();
|
|
833
|
+
const relations = storage.getRelations();
|
|
834
|
+
const schema = storage.getSchema();
|
|
835
|
+
const pivotTables = /* @__PURE__ */ new Set();
|
|
836
|
+
for (const fields of Object.values(relations)) {
|
|
837
|
+
for (const def of Object.values(fields)) {
|
|
838
|
+
if (def.type === "many2many") pivotTables.add(def.through);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const fkFields = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
843
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
844
|
+
if (def.type !== "many2many") {
|
|
845
|
+
if (!fkFields.has(source)) fkFields.set(source, /* @__PURE__ */ new Set());
|
|
846
|
+
fkFields.get(source).add(key);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const entities = Object.entries(data).map(([name, items]) => {
|
|
851
|
+
const sample = items[0] ?? {};
|
|
852
|
+
const schemaFields = schema[name] ?? {};
|
|
853
|
+
const allFields = /* @__PURE__ */ new Set([...Object.keys(sample), ...Object.keys(schemaFields)]);
|
|
854
|
+
const fields = [...allFields].map((fieldName) => ({
|
|
855
|
+
name: fieldName,
|
|
856
|
+
type: schemaFields[fieldName]?.type ?? inferType(sample[fieldName]),
|
|
857
|
+
pk: fieldName === "id",
|
|
858
|
+
fk: fkFields.get(name)?.has(fieldName) ?? false,
|
|
859
|
+
required: schemaFields[fieldName]?.required ?? false
|
|
860
|
+
}));
|
|
861
|
+
fields.sort((a, b) => {
|
|
862
|
+
if (a.pk) return -1;
|
|
863
|
+
if (b.pk) return 1;
|
|
864
|
+
if (a.fk && !b.fk) return 1;
|
|
865
|
+
if (!a.fk && b.fk) return -1;
|
|
866
|
+
return 0;
|
|
867
|
+
});
|
|
868
|
+
return { name, fields, isPivot: pivotTables.has(name) };
|
|
869
|
+
});
|
|
870
|
+
const erRelations = [];
|
|
871
|
+
for (const [source, fields] of Object.entries(relations)) {
|
|
872
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
873
|
+
if (def.type === "many2many") {
|
|
874
|
+
erRelations.push({
|
|
875
|
+
source,
|
|
876
|
+
target: def.target,
|
|
877
|
+
type: "many2many",
|
|
878
|
+
label: def.through,
|
|
879
|
+
through: def.through,
|
|
880
|
+
foreignKey: def.foreignKey,
|
|
881
|
+
otherKey: def.otherKey
|
|
882
|
+
});
|
|
883
|
+
} else {
|
|
884
|
+
erRelations.push({
|
|
885
|
+
source,
|
|
886
|
+
target: def.target,
|
|
887
|
+
type: def.type,
|
|
888
|
+
label: key,
|
|
889
|
+
carDirect: def.carDirect,
|
|
890
|
+
carInverse: def.carInverse
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return { entities, relations: erRelations };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/router/templates/about.diagram.canvas.ts
|
|
899
|
+
init_esm_shims();
|
|
900
|
+
function erDiagramSection(data) {
|
|
901
|
+
if (!data.entities.length) return "";
|
|
902
|
+
const json = JSON.stringify(data);
|
|
903
|
+
return (
|
|
904
|
+
/*html*/
|
|
905
|
+
`
|
|
906
|
+
<h2>Data Model</h2>
|
|
907
|
+
|
|
908
|
+
<div class="card" style="padding:0;overflow:hidden;position:relative;">
|
|
909
|
+
<canvas id="er-canvas" style="width:100%;display:block;cursor:grab;"></canvas>
|
|
910
|
+
<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>
|
|
911
|
+
</div>
|
|
912
|
+
|
|
913
|
+
<script>${/*js*/
|
|
914
|
+
`
|
|
915
|
+
(function(){
|
|
916
|
+
|
|
917
|
+
var D=${json};
|
|
918
|
+
|
|
919
|
+
// \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
|
|
920
|
+
var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=8,BO=14,LGH=56;
|
|
921
|
+
|
|
922
|
+
var C={bg:'#0d1117',nb:'#161b22',nh:'#1c2128',pb:'#0d1117',ph:'#161b22',
|
|
923
|
+
bo:'#30363d',bs:'#58a6ff',tx:'#e6edf3',mu:'#7d8590',
|
|
924
|
+
pk:'#e3b341',fk:'#79c0ff',m2o:'#58a6ff',o2o:'#34d399',m2m:'#818cf8'};
|
|
925
|
+
|
|
926
|
+
// \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
|
|
927
|
+
var cv = document.getElementById('er-canvas');
|
|
928
|
+
var dpr = window.devicePixelRatio||1;
|
|
929
|
+
var ctx = cv.getContext('2d');
|
|
930
|
+
|
|
931
|
+
function resize(h){
|
|
932
|
+
if(h)cv.style.height = h + 'px';
|
|
933
|
+
var r = cv.getBoundingClientRect();
|
|
934
|
+
cv.width = Math.round(r.width*dpr);
|
|
935
|
+
cv.height = Math.round(r.height*dpr);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// \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
|
|
939
|
+
var nodes = D.entities.map(function(e){
|
|
940
|
+
return{e:e,x:0,y:0,w:NW,h:HH+e.fields.length*FH+6};
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
var nIdx={};D.entities.forEach(function(e,i){nIdx[e.name]=i;});
|
|
944
|
+
|
|
945
|
+
var adj=nodes.map(function(){return[];}); // forward
|
|
946
|
+
var radj=nodes.map(function(){return[];}); // reverse
|
|
947
|
+
|
|
948
|
+
D.relations.forEach(function(r){
|
|
949
|
+
if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
|
|
950
|
+
var s=nIdx[r.source],p=nIdx[r.through],t=nIdx[r.target];
|
|
951
|
+
if(s!=null&&p!=null&&s!==p){adj[s].push(p);radj[p].push(s);}
|
|
952
|
+
if(t!=null&&p!=null&&t!==p){adj[t].push(p);radj[p].push(t);}
|
|
953
|
+
}else{
|
|
954
|
+
var s=nIdx[r.source],t=nIdx[r.target];
|
|
955
|
+
if(s==null||t==null||s===t)return;
|
|
956
|
+
adj[s].push(t);radj[t].push(s);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// \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
|
|
961
|
+
var groups=[];
|
|
962
|
+
|
|
963
|
+
function median(arr){
|
|
964
|
+
if(!arr.length)return 0;
|
|
965
|
+
var s=[].concat(arr).sort(function(a,b){return a-b;});
|
|
966
|
+
return s[Math.floor(s.length/2)];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function computeLayout(W,H){
|
|
970
|
+
var layer=nodes.map(function(){return 0;});
|
|
971
|
+
var inDeg=nodes.map(function(){return 0;});
|
|
972
|
+
adj.forEach(function(ts){ts.forEach(function(t){inDeg[t]++;});});
|
|
973
|
+
var q=[],qi=0;
|
|
974
|
+
for(var i=0;i<nodes.length;i++)if(inDeg[i]===0)q.push(i);
|
|
975
|
+
if(!q.length)for(var i=0;i<nodes.length;i++)q.push(i);
|
|
976
|
+
while(qi<q.length){
|
|
977
|
+
var u=q[qi++];
|
|
978
|
+
adj[u].forEach(function(v){if(layer[v]<=layer[u]){layer[v]=layer[u]+1;q.push(v);}});
|
|
979
|
+
}
|
|
980
|
+
var maxL=layer.reduce(function(m,l){return Math.max(m,l);},0);
|
|
981
|
+
groups=[];
|
|
982
|
+
for(var l=0;l<=maxL;l++)groups.push([]);
|
|
983
|
+
for(var i=0;i<nodes.length;i++)groups[layer[i]].push(i);
|
|
984
|
+
for(var l=1;l<=maxL;l++){
|
|
985
|
+
var pp={};groups[l-1].forEach(function(i,p){pp[i]=p;});
|
|
986
|
+
groups[l].sort(function(a,b){
|
|
987
|
+
return median(radj[a].map(function(x){return pp[x]!=null?pp[x]:0;}))-
|
|
988
|
+
median(radj[b].map(function(x){return pp[x]!=null?pp[x]:0;}));
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
for(var l=maxL-1;l>=0;l--){
|
|
992
|
+
var np={};groups[l+1].forEach(function(i,p){np[i]=p;});
|
|
993
|
+
groups[l].sort(function(a,b){
|
|
994
|
+
return median(adj[a].map(function(x){return np[x]!=null?np[x]:0;}))-
|
|
995
|
+
median(adj[b].map(function(x){return np[x]!=null?np[x]:0;}));
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
var colW=NW+COL_GAP;
|
|
999
|
+
var totalW=(maxL+1)*colW-COL_GAP;
|
|
1000
|
+
var startX=Math.max(20,(W-totalW)/2);
|
|
1001
|
+
for(var l=0;l<=maxL;l++){
|
|
1002
|
+
var g=groups[l];
|
|
1003
|
+
var totalH=g.reduce(function(s,i){return s+nodes[i].h;},0)+(g.length-1)*ROW_GAP;
|
|
1004
|
+
var y=Math.max(20,(H-totalH)/2);
|
|
1005
|
+
g.forEach(function(i){nodes[i].x=startX+l*colW;nodes[i].y=y;y+=nodes[i].h+ROW_GAP;});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function requiredH(){
|
|
1010
|
+
return Math.max(340,Math.min(720,groups.reduce(function(m,g){
|
|
1011
|
+
return Math.max(m,g.reduce(function(s,i){return s+nodes[i].h;},0)+(g.length-1)*ROW_GAP+LGH+70);
|
|
1012
|
+
},300)));
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// \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
|
|
1016
|
+
var pan={x:0,y:0},zm=1;
|
|
1017
|
+
var origPos=[];
|
|
1018
|
+
|
|
1019
|
+
function savePos(){origPos=nodes.map(function(n){return{x:n.x,y:n.y};});}
|
|
1020
|
+
|
|
1021
|
+
function fitView(W,H){
|
|
1022
|
+
var x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;
|
|
1023
|
+
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);});
|
|
1024
|
+
var pad=28,gW=x1-x0+pad*2,gH=y1-y0+pad*2;
|
|
1025
|
+
zm=Math.min(W/gW,(H-LGH)/gH,1);
|
|
1026
|
+
pan.x=(W-gW*zm)/2+pad*zm-x0*zm;
|
|
1027
|
+
pan.y=((H-LGH)-gH*zm)/2+pad*zm-y0*zm;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// \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
|
|
1031
|
+
function rr(x,y,w,h,r){
|
|
1032
|
+
if(ctx.roundRect){ctx.beginPath();ctx.roundRect(x,y,w,h,r);return;}
|
|
1033
|
+
ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);
|
|
1034
|
+
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();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function edgePt(n,tx,ty){
|
|
1038
|
+
var cx=n.x+n.w/2,cy=n.y+n.h/2,dx=tx-cx,dy=ty-cy;
|
|
1039
|
+
if(!dx&&!dy)return{x:cx,y:cy};
|
|
1040
|
+
var s=Math.min((n.w/2)/Math.abs(dx||.001),(n.h/2)/Math.abs(dy||.001));
|
|
1041
|
+
return{x:cx+dx*s,y:cy+dy*s};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
var badges=[];
|
|
1045
|
+
|
|
1046
|
+
function pushBadge(x,y,ux,uy,many,col){
|
|
1047
|
+
badges.push({x:x,y:y,ux:ux,uy:uy,many:many,col:col});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Badge: solid colored circle; crow's foot (many) or double-bar (one) in dark ink inside.
|
|
1051
|
+
// ux,uy points outward from the node \u2014 badge is offset BO px into the gap.
|
|
1052
|
+
// Crow's foot tines open toward the node (-x in rotated frame), tip toward line (+x).
|
|
1053
|
+
function drawBadge(b){
|
|
1054
|
+
var bx=b.x+b.ux*BO,by=b.y+b.uy*BO;
|
|
1055
|
+
ctx.fillStyle=b.col;ctx.strokeStyle=b.col;ctx.lineWidth=1;
|
|
1056
|
+
ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.stroke();
|
|
1057
|
+
ctx.save();ctx.translate(bx,by);ctx.rotate(Math.atan2(b.uy,b.ux));
|
|
1058
|
+
ctx.strokeStyle=C.bg;ctx.lineWidth=1.4;ctx.lineCap='round';
|
|
1059
|
+
ctx.beginPath();
|
|
1060
|
+
if(b.many){
|
|
1061
|
+
ctx.moveTo(-5,-4);ctx.lineTo(5,0); // upper tine \u2192 tip
|
|
1062
|
+
ctx.moveTo(-5,4);ctx.lineTo(5,0); // lower tine \u2192 tip
|
|
1063
|
+
ctx.moveTo(-5,0);ctx.lineTo(5,0); // center line
|
|
1064
|
+
ctx.moveTo(-5,-4);ctx.lineTo(-5,4); // base bar (near node)
|
|
1065
|
+
}else{
|
|
1066
|
+
ctx.moveTo(-3,-5);ctx.lineTo(-3,5); // near bar
|
|
1067
|
+
ctx.moveTo(2,-5);ctx.lineTo(2,5); // far bar
|
|
1068
|
+
}
|
|
1069
|
+
ctx.stroke();
|
|
1070
|
+
ctx.restore();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function ecol(t){return t==='one2one'?C.o2o:t==='many2many'?C.m2m:C.m2o;}
|
|
1074
|
+
|
|
1075
|
+
// offset: pixels to shift perpendicular to the edge direction (for parallel edges)
|
|
1076
|
+
function drawEdgeLine(a,b,col,dashed,label,manyA,manyB,off){
|
|
1077
|
+
var p1=edgePt(a,b.x+b.w/2,b.y+b.h/2);
|
|
1078
|
+
var p2=edgePt(b,a.x+a.w/2,a.y+a.h/2);
|
|
1079
|
+
var dx=p2.x-p1.x,dy=p2.y-p1.y,d=Math.sqrt(dx*dx+dy*dy)||1;
|
|
1080
|
+
var ux=dx/d,uy=dy/d;
|
|
1081
|
+
var px=-uy*off,py=ux*off; // perpendicular shift
|
|
1082
|
+
var q1x=p1.x+px,q1y=p1.y+py,q2x=p2.x+px,q2y=p2.y+py;
|
|
1083
|
+
ctx.strokeStyle=col+'55';ctx.lineWidth=1.5;
|
|
1084
|
+
ctx.setLineDash(dashed?[5,4]:[]);
|
|
1085
|
+
ctx.beginPath();ctx.moveTo(q1x,q1y);ctx.lineTo(q2x,q2y);ctx.stroke();
|
|
1086
|
+
ctx.setLineDash([]);
|
|
1087
|
+
pushBadge(q1x,q1y,ux,uy,manyA,col);
|
|
1088
|
+
pushBadge(q2x,q2y,-ux,-uy,manyB,col);
|
|
1089
|
+
if(label){
|
|
1090
|
+
var mx=(q1x+q2x)/2,my=(q1y+q2y)/2;
|
|
1091
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
|
|
1092
|
+
ctx.fillText(label,mx-uy*14,my+ux*14);
|
|
1093
|
+
ctx.textBaseline='alphabetic';
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// canonical key for a node pair regardless of direction
|
|
1098
|
+
function pairKey(a,b){
|
|
1099
|
+
var ai=nodes.indexOf(a),bi=nodes.indexOf(b);
|
|
1100
|
+
return ai<bi?ai+'-'+bi:bi+'-'+ai;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function collectEdges(){
|
|
1104
|
+
var edges=[];
|
|
1105
|
+
D.relations.forEach(function(r){
|
|
1106
|
+
var col=ecol(r.type);
|
|
1107
|
+
if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
|
|
1108
|
+
var sn=nodes[nIdx[r.source]],pn=nodes[nIdx[r.through]],tn=nodes[nIdx[r.target]];
|
|
1109
|
+
if(sn&&pn)edges.push({a:sn,b:pn,col:col,dashed:true,label:r.foreignKey||'',manyA:false,manyB:true,off:0});
|
|
1110
|
+
if(tn&&pn)edges.push({a:tn,b:pn,col:col,dashed:true,label:r.otherKey||'',manyA:false,manyB:true,off:0});
|
|
1111
|
+
}else{
|
|
1112
|
+
var sn=nodes[nIdx[r.source]],tn=nodes[nIdx[r.target]];
|
|
1113
|
+
if(sn&&tn&&sn!==tn){
|
|
1114
|
+
var manyA=r.carInverse?r.carInverse.endsWith('n'):r.type!=='one2one';
|
|
1115
|
+
var manyB=r.carDirect?r.carDirect.endsWith('n'):r.type==='many2many';
|
|
1116
|
+
edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,manyA:manyA,manyB:manyB,off:0});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
// assign perpendicular offsets to parallel edges (same node pair)
|
|
1121
|
+
var cnt={},idx={};
|
|
1122
|
+
edges.forEach(function(e){var k=pairKey(e.a,e.b);cnt[k]=(cnt[k]||0)+1;});
|
|
1123
|
+
edges.forEach(function(e){
|
|
1124
|
+
var k=pairKey(e.a,e.b),n=cnt[k];
|
|
1125
|
+
var i=idx[k]=(idx[k]||0);
|
|
1126
|
+
e.off=n>1?(i-(n-1)/2)*16:0;
|
|
1127
|
+
idx[k]++;
|
|
1128
|
+
});
|
|
1129
|
+
return edges;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function drawNode(n){
|
|
1133
|
+
var x=n.x,y=n.y,w=n.w,h=n.h,e=n.e;
|
|
1134
|
+
var bg=e.isPivot?C.pb:C.nb,hd=e.isPivot?C.ph:C.nh;
|
|
1135
|
+
ctx.save();ctx.shadowColor='rgba(0,0,0,0.45)';ctx.shadowBlur=14;ctx.shadowOffsetY=4;
|
|
1136
|
+
ctx.fillStyle=bg;rr(x,y,w,h,7);ctx.fill();ctx.restore();
|
|
1137
|
+
ctx.fillStyle=bg;rr(x,y,w,h,7);ctx.fill();
|
|
1138
|
+
ctx.strokeStyle=n===drag?C.bs:C.bo;ctx.lineWidth=1.2;rr(x,y,w,h,7);ctx.stroke();
|
|
1139
|
+
ctx.fillStyle=hd;rr(x,y,w,HH,7);ctx.fill();ctx.fillRect(x,y+HH-7,w,7);
|
|
1140
|
+
ctx.strokeStyle=C.bo;ctx.lineWidth=1;
|
|
1141
|
+
ctx.beginPath();ctx.moveTo(x,y+HH);ctx.lineTo(x+w,y+HH);ctx.stroke();
|
|
1142
|
+
ctx.fillStyle=e.isPivot?C.mu:C.tx;
|
|
1143
|
+
ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
|
|
1144
|
+
ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1145
|
+
ctx.fillText(e.name,x+FP,y+HH/2);
|
|
1146
|
+
if(e.isPivot){
|
|
1147
|
+
ctx.fillStyle='#3d444d';ctx.font='9px monospace';ctx.textAlign='right';
|
|
1148
|
+
ctx.fillText('pivot',x+w-FP,y+HH/2);
|
|
1149
|
+
}
|
|
1150
|
+
e.fields.forEach(function(f,fi){
|
|
1151
|
+
var fy=y+HH+fi*FH+FH/2+3;
|
|
1152
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
|
|
1153
|
+
ctx.fillText(f.type,x+w-FP-(f.required&&!f.pk?14:0),fy);
|
|
1154
|
+
if(f.pk||f.fk){
|
|
1155
|
+
var tag=f.pk?'PK':'FK',tc=f.pk?C.pk:C.fk;
|
|
1156
|
+
ctx.fillStyle=tc+'22';ctx.strokeStyle=tc+'66';ctx.lineWidth=1;
|
|
1157
|
+
rr(x+FP,fy-6,12,12,2);ctx.fill();ctx.stroke();
|
|
1158
|
+
ctx.fillStyle=tc;ctx.font='7px monospace';ctx.textAlign='center';
|
|
1159
|
+
ctx.fillText(tag,x+FP+6,fy+0.5);
|
|
1160
|
+
}
|
|
1161
|
+
ctx.fillStyle=f.pk?C.pk:f.fk?C.fk:C.tx;
|
|
1162
|
+
ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
|
|
1163
|
+
ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1164
|
+
ctx.fillText(f.name,x+FP+(f.pk||f.fk?16:0),fy);
|
|
1165
|
+
if(f.required&&!f.pk){
|
|
1166
|
+
ctx.fillStyle='#f85149';ctx.beginPath();
|
|
1167
|
+
ctx.arc(x+w-FP-6,fy,2.5,0,Math.PI*2);ctx.fill();
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
ctx.textBaseline='alphabetic';
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function drawLegend(){
|
|
1174
|
+
var cvW=cv.width/dpr,cvH=cv.height/dpr;
|
|
1175
|
+
var y0=cvH-LGH,y1=cvH-LGH+18,y2=cvH-LGH+38;
|
|
1176
|
+
|
|
1177
|
+
ctx.fillStyle='#161b22';
|
|
1178
|
+
ctx.fillRect(0,y0,cvW,LGH);
|
|
1179
|
+
ctx.strokeStyle='#30363d';ctx.lineWidth=1;
|
|
1180
|
+
ctx.beginPath();ctx.moveTo(0,y0);ctx.lineTo(cvW,y0);ctx.stroke();
|
|
1181
|
+
|
|
1182
|
+
// Row 1: relation line types
|
|
1183
|
+
var types=[
|
|
1184
|
+
{c:C.m2o,l:'many2one',d:false},
|
|
1185
|
+
{c:C.o2o,l:'one2one',d:false},
|
|
1186
|
+
{c:C.m2m,l:'many2many',d:true}
|
|
1187
|
+
];
|
|
1188
|
+
ctx.textAlign='left';ctx.textBaseline='middle';ctx.font='10px monospace';
|
|
1189
|
+
var x=14;
|
|
1190
|
+
types.forEach(function(it){
|
|
1191
|
+
ctx.strokeStyle=it.c;ctx.lineWidth=1.5;ctx.setLineDash(it.d?[4,3]:[]);
|
|
1192
|
+
ctx.beginPath();ctx.moveTo(x,y1);ctx.lineTo(x+14,y1);ctx.stroke();
|
|
1193
|
+
ctx.setLineDash([]);
|
|
1194
|
+
ctx.fillStyle=C.mu;
|
|
1195
|
+
ctx.fillText(it.l,x+18,y1);
|
|
1196
|
+
x+=18+ctx.measureText(it.l).width+22;
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Row 2: cardinality symbols
|
|
1200
|
+
function badge2(cx,cy,many,col){
|
|
1201
|
+
ctx.fillStyle=col;ctx.strokeStyle=col;ctx.lineWidth=1;
|
|
1202
|
+
ctx.beginPath();ctx.arc(cx,cy,6,0,Math.PI*2);ctx.fill();
|
|
1203
|
+
ctx.save();ctx.translate(cx,cy);
|
|
1204
|
+
ctx.strokeStyle=C.bg;ctx.lineWidth=1.2;ctx.lineCap='round';
|
|
1205
|
+
ctx.beginPath();
|
|
1206
|
+
if(many){
|
|
1207
|
+
ctx.moveTo(-4,-3);ctx.lineTo(4,0);ctx.moveTo(-4,3);ctx.lineTo(4,0);
|
|
1208
|
+
ctx.moveTo(-4,0);ctx.lineTo(4,0);ctx.moveTo(-4,-3);ctx.lineTo(-4,3);
|
|
1209
|
+
}else{
|
|
1210
|
+
ctx.moveTo(-2,-3);ctx.lineTo(-2,3);ctx.moveTo(2,-3);ctx.lineTo(2,3);
|
|
1211
|
+
}
|
|
1212
|
+
ctx.stroke();ctx.restore();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1216
|
+
x=14;
|
|
1217
|
+
var pairs=[
|
|
1218
|
+
{a:false,b:true,col:C.m2o,l:'many2one (N:1)'},
|
|
1219
|
+
{a:false,b:false,col:C.o2o,l:'one2one (1:1)'},
|
|
1220
|
+
{a:false,b:true,col:C.m2m,l:'pivot edge (1:N)'}
|
|
1221
|
+
];
|
|
1222
|
+
pairs.forEach(function(p){
|
|
1223
|
+
ctx.strokeStyle=p.col+'55';ctx.lineWidth=1.2;ctx.setLineDash(p.col===C.m2m?[4,3]:[]);
|
|
1224
|
+
ctx.beginPath();ctx.moveTo(x+8,y2);ctx.lineTo(x+56,y2);ctx.stroke();
|
|
1225
|
+
ctx.setLineDash([]);
|
|
1226
|
+
badge2(x+8,y2,p.a,p.col);
|
|
1227
|
+
badge2(x+56,y2,p.b,p.col);
|
|
1228
|
+
ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
|
|
1229
|
+
ctx.fillText(p.l,x+66,y2);
|
|
1230
|
+
x+=66+ctx.measureText(p.l).width+24;
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
ctx.fillStyle='#3d444d';ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
|
|
1234
|
+
ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y2);
|
|
1235
|
+
ctx.textBaseline='alphabetic';
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function draw(){
|
|
1239
|
+
var W=cv.width/dpr,H=cv.height/dpr;
|
|
1240
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
1241
|
+
ctx.clearRect(0,0,W,H);
|
|
1242
|
+
ctx.fillStyle=C.bg;ctx.fillRect(0,0,W,H);
|
|
1243
|
+
ctx.save();ctx.translate(pan.x,pan.y);ctx.scale(zm,zm);
|
|
1244
|
+
badges=[];
|
|
1245
|
+
var edges=collectEdges();
|
|
1246
|
+
edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.manyA,e.manyB,e.off);});
|
|
1247
|
+
nodes.forEach(function(n){drawNode(n);});
|
|
1248
|
+
badges.forEach(function(b){drawBadge(b);});
|
|
1249
|
+
ctx.restore();
|
|
1250
|
+
drawLegend();
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// \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
|
|
1254
|
+
var drag=null,dox=0,doy=0,isPan=false,ps={x:0,y:0};
|
|
1255
|
+
|
|
1256
|
+
function toW(mx,my){return{x:(mx-pan.x)/zm,y:(my-pan.y)/zm};}
|
|
1257
|
+
function hitTest(wx,wy){
|
|
1258
|
+
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;}
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
cv.addEventListener('mousedown',function(ev){
|
|
1263
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1264
|
+
var w=toW(mx,my),h=hitTest(w.x,w.y);
|
|
1265
|
+
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};}
|
|
1266
|
+
cv.style.cursor='grabbing';
|
|
1267
|
+
});
|
|
1268
|
+
window.addEventListener('mousemove',function(ev){
|
|
1269
|
+
if(!drag&&!isPan)return;
|
|
1270
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1271
|
+
if(drag){var w=toW(mx,my);drag.x=w.x-dox;drag.y=w.y-doy;}
|
|
1272
|
+
else{pan.x=mx-ps.x;pan.y=my-ps.y;}
|
|
1273
|
+
draw();
|
|
1274
|
+
});
|
|
1275
|
+
window.addEventListener('mouseup',function(){drag=null;isPan=false;cv.style.cursor='grab';});
|
|
1276
|
+
cv.addEventListener('wheel',function(ev){
|
|
1277
|
+
ev.preventDefault();
|
|
1278
|
+
var r=cv.getBoundingClientRect(),mx=ev.clientX-r.left,my=ev.clientY-r.top;
|
|
1279
|
+
var d=ev.deltaY<0?1.1:0.91,nz=Math.max(0.25,Math.min(3,zm*d));
|
|
1280
|
+
pan.x=mx-(mx-pan.x)*(nz/zm);pan.y=my-(my-pan.y)*(nz/zm);zm=nz;draw();
|
|
1281
|
+
},{passive:false});
|
|
1282
|
+
cv.addEventListener('dblclick',function(){
|
|
1283
|
+
nodes.forEach(function(n,i){n.x=origPos[i].x;n.y=origPos[i].y;});
|
|
1284
|
+
var r=cv.getBoundingClientRect();fitView(r.width,r.height);draw();
|
|
1285
|
+
});
|
|
1286
|
+
window.addEventListener('resize',function(){
|
|
1287
|
+
resize();var r=cv.getBoundingClientRect();
|
|
1288
|
+
computeLayout(r.width,r.height);savePos();fitView(r.width,r.height);draw();
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
document.getElementById('er-dl').addEventListener('click',function(){
|
|
1292
|
+
var SCALE=3;
|
|
1293
|
+
var cw=cv.width/dpr,ch=cv.height/dpr;
|
|
1294
|
+
var oc=document.createElement('canvas');
|
|
1295
|
+
oc.width=Math.round(cw*SCALE);oc.height=Math.round(ch*SCALE);
|
|
1296
|
+
var sav={cv:cv,ctx:ctx,dpr:dpr};
|
|
1297
|
+
cv=oc;ctx=oc.getContext('2d');dpr=SCALE;
|
|
1298
|
+
draw();
|
|
1299
|
+
cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;
|
|
1300
|
+
var a=document.createElement('a');
|
|
1301
|
+
a.download='er-diagram.png';a.href=oc.toDataURL('image/png');a.click();
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
// \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
|
|
1305
|
+
requestAnimationFrame(function(){
|
|
1306
|
+
resize();
|
|
1307
|
+
var r=cv.getBoundingClientRect();
|
|
1308
|
+
computeLayout(r.width,r.height);
|
|
1309
|
+
var h=requiredH();
|
|
1310
|
+
resize(h);r=cv.getBoundingClientRect();
|
|
1311
|
+
computeLayout(r.width,r.height);
|
|
1312
|
+
savePos();fitView(r.width,r.height);draw();
|
|
1313
|
+
});
|
|
1314
|
+
})();
|
|
1315
|
+
`}</script>`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
649
1319
|
// src/router/templates/about.template.ts
|
|
650
1320
|
var _dir = dirname(fileURLToPath2(import.meta.url));
|
|
651
1321
|
var LOGO_SRC = (() => {
|
|
652
1322
|
try {
|
|
653
|
-
const buf = readFileSync(join(_dir, "../../assets/
|
|
654
|
-
return `data:image/
|
|
1323
|
+
const buf = readFileSync(join(_dir, "../../assets/yrest-lockup.svg"));
|
|
1324
|
+
return `data:image/svg+xml;base64,${buf.toString("base64")}`;
|
|
655
1325
|
} catch {
|
|
656
1326
|
return "";
|
|
657
1327
|
}
|
|
@@ -662,6 +1332,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
662
1332
|
const customRoutes = storage.getRoutes();
|
|
663
1333
|
const base = options.base;
|
|
664
1334
|
const host = `http://${options.host}:${options.port}`;
|
|
1335
|
+
const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
|
|
665
1336
|
const modes = [];
|
|
666
1337
|
if (options.watch) modes.push(badge("watch", "#38bdf8", "#38bdf818"));
|
|
667
1338
|
if (options.readonly) modes.push(badge("readonly", "#94a3b8", "#94a3b818"));
|
|
@@ -672,9 +1343,12 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
672
1343
|
if (handlers.size > 0) modes.push(badge(`handlers \xB7 ${handlers.size}`, "#f0883e", "#f0883e18"));
|
|
673
1344
|
if (options.idStrategy !== "increment")
|
|
674
1345
|
modes.push(badge(`id \xB7 ${options.idStrategy}`, "#a371f7", "#a371f718"));
|
|
1346
|
+
const erDiagram = erDiagramSection(generateERData(storage));
|
|
675
1347
|
const accordions = collections.map((col, i) => resourceAccordion(col, base, i === 0)).join("");
|
|
676
1348
|
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.`;
|
|
677
|
-
return
|
|
1349
|
+
return (
|
|
1350
|
+
/*html*/
|
|
1351
|
+
`<!DOCTYPE html>
|
|
678
1352
|
<html lang="en">
|
|
679
1353
|
<head>
|
|
680
1354
|
<meta charset="UTF-8">
|
|
@@ -701,20 +1375,30 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
701
1375
|
width: 100%;
|
|
702
1376
|
background: linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a2332 100%);
|
|
703
1377
|
border-bottom: 1px solid var(--border);
|
|
704
|
-
padding:
|
|
1378
|
+
padding: 20px 32px 16px;
|
|
705
1379
|
}
|
|
706
|
-
.banner-inner {
|
|
707
|
-
.banner
|
|
1380
|
+
.banner-inner { margin: 0 auto; display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; }
|
|
1381
|
+
.banner-left { flex:1; min-width:0; }
|
|
1382
|
+
.banner h1 { font-size: clamp(24px, 4vw, 40px); font-weight: 800; letter-spacing: -1.5px; line-height: 1; }
|
|
708
1383
|
.banner h1 .y { color: var(--text); }
|
|
709
1384
|
.banner h1 .rest { color: var(--accent); }
|
|
710
|
-
.banner p { color: var(--text-muted); margin-top:
|
|
711
|
-
.banner-meta { display: flex; gap:
|
|
712
|
-
.banner-meta span { color: var(--text-muted); font-size:
|
|
1385
|
+
.banner p { color: var(--text-muted); margin-top: 6px; font-size: 14px; }
|
|
1386
|
+
.banner-meta { display: flex; gap: 20px; margin-top: 10px; flex-wrap: wrap; }
|
|
1387
|
+
.banner-meta span { color: var(--text-muted); font-size: 12px; }
|
|
713
1388
|
.banner-meta span strong { color: var(--text); font-family: monospace; }
|
|
1389
|
+
.api-links { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
|
1390
|
+
.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; }
|
|
1391
|
+
.btn-api:hover { border-color:var(--border-hi); color:var(--text); }
|
|
1392
|
+
.btn-swagger { border-color:var(--accent); color:var(--accent); }
|
|
1393
|
+
.btn-swagger:hover { background:#58a6ff18; }
|
|
714
1394
|
|
|
715
1395
|
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
716
|
-
.wrap {
|
|
1396
|
+
.wrap { padding: 24px 32px 48px; }
|
|
717
1397
|
h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin: 32px 0 12px; }
|
|
1398
|
+
.about-grid { display:grid; grid-template-columns:5fr 7fr; gap:28px; align-items:start; }
|
|
1399
|
+
@media(max-width:960px) { .about-grid { grid-template-columns:1fr; } }
|
|
1400
|
+
.col-left, .col-right { min-width:0; }
|
|
1401
|
+
.col-left > h2:first-child, .col-right > h2:first-child { margin-top:0; }
|
|
718
1402
|
|
|
719
1403
|
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
720
1404
|
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 24px; }
|
|
@@ -730,10 +1414,8 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
730
1414
|
.modes { display: flex; gap: 8px; flex-wrap: wrap; min-height: 28px; align-items: center; }
|
|
731
1415
|
.badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
732
1416
|
|
|
733
|
-
/* \u2500\u2500 Endpoints
|
|
734
|
-
.endpoints-grid { display:
|
|
735
|
-
@media (max-width: 860px) { .endpoints-grid { grid-template-columns: 1fr; } }
|
|
736
|
-
.nested-card { grid-column: 1 / -1; }
|
|
1417
|
+
/* \u2500\u2500 Endpoints list \u2500\u2500 */
|
|
1418
|
+
.endpoints-grid { display: flex; flex-direction: column; gap: 10px; }
|
|
737
1419
|
|
|
738
1420
|
/* \u2500\u2500 Accordion (details/summary) \u2500\u2500 */
|
|
739
1421
|
.resource-card {
|
|
@@ -798,64 +1480,80 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
|
|
|
798
1480
|
|
|
799
1481
|
<div class="banner">
|
|
800
1482
|
<div class="banner-inner">
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
<
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1483
|
+
<div class="banner-left">
|
|
1484
|
+
${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>`}
|
|
1485
|
+
<p>Zero-config REST API mock server</p>
|
|
1486
|
+
<div class="banner-meta">
|
|
1487
|
+
<span>URL <strong>${host}</strong></span>
|
|
1488
|
+
<span>Base <strong>${base || "/"}</strong></span>
|
|
1489
|
+
<span>File <strong>${options.file}</strong></span>
|
|
1490
|
+
<span>Collections <strong>${collections.length}</strong></span>
|
|
1491
|
+
</div>
|
|
1492
|
+
</div>
|
|
1493
|
+
<div class="api-links">
|
|
1494
|
+
<a href="/_openapi" target="_blank" class="btn-api">OpenAPI YAML</a>
|
|
1495
|
+
<a href="/_openapi.json" target="_blank" class="btn-api">OpenAPI JSON</a>
|
|
1496
|
+
<a href="${swaggerUrl}" target="_blank" class="btn-api btn-swagger">Swagger UI \u2197</a>
|
|
808
1497
|
</div>
|
|
809
1498
|
</div>
|
|
810
1499
|
</div>
|
|
811
1500
|
|
|
812
1501
|
<div class="wrap">
|
|
1502
|
+
<div class="about-grid">
|
|
1503
|
+
|
|
1504
|
+
<div class="col-left">
|
|
1505
|
+
<h2>Active Modes</h2>
|
|
1506
|
+
<div class="card">
|
|
1507
|
+
<div class="modes">${modes.length ? modes.join(" ") : `<span style="color:var(--text-muted);font-size:13px">none</span>`}</div>
|
|
1508
|
+
${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>` : ""}
|
|
1509
|
+
</div>
|
|
1510
|
+
|
|
1511
|
+
<h2>Endpoints</h2>
|
|
1512
|
+
<div class="endpoints-grid">
|
|
1513
|
+
${accordions}
|
|
1514
|
+
${nestedRoutesAccordion(relations, base)}
|
|
1515
|
+
${options.snapshot ? snapshotAccordion() : ""}
|
|
1516
|
+
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
1517
|
+
${handlersAccordion(handlers, customRoutes, base)}
|
|
1518
|
+
</div>
|
|
1519
|
+
|
|
1520
|
+
<h2>Query Parameters</h2>
|
|
1521
|
+
<div class="card">
|
|
1522
|
+
<table class="param-table">
|
|
1523
|
+
<thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
|
|
1524
|
+
<tbody>
|
|
1525
|
+
<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>
|
|
1526
|
+
<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>
|
|
1527
|
+
<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>
|
|
1528
|
+
<tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
|
|
1529
|
+
<tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
|
|
1530
|
+
<tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
|
|
1531
|
+
<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>
|
|
1532
|
+
<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>
|
|
1533
|
+
<tr><td><code>?_page & ?_limit</code></td><td><code>?_page=2&_limit=10</code></td><td>${paginationDesc}</td></tr>
|
|
1534
|
+
<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>
|
|
1535
|
+
<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>
|
|
1536
|
+
<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>
|
|
1537
|
+
</tbody>
|
|
1538
|
+
</table>
|
|
1539
|
+
</div>
|
|
1540
|
+
</div>
|
|
813
1541
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
</div>
|
|
819
|
-
|
|
820
|
-
<h2>Endpoints</h2>
|
|
821
|
-
<div class="endpoints-grid">
|
|
822
|
-
${accordions}
|
|
823
|
-
${nestedRoutesAccordion(relations, base)}
|
|
824
|
-
${options.snapshot ? snapshotAccordion() : ""}
|
|
825
|
-
${customRoutesAccordion(customRoutes, base, handlers)}
|
|
826
|
-
${handlersAccordion(handlers, customRoutes, base)}
|
|
827
|
-
</div>
|
|
1542
|
+
<div class="col-right">
|
|
1543
|
+
${erDiagram}
|
|
1544
|
+
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
1545
|
+
</div>
|
|
828
1546
|
|
|
829
|
-
<h2>Query Parameters</h2>
|
|
830
|
-
<div class="card">
|
|
831
|
-
<table class="param-table">
|
|
832
|
-
<thead><tr><th>Param</th><th>Example</th><th>Description</th></tr></thead>
|
|
833
|
-
<tbody>
|
|
834
|
-
<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>
|
|
835
|
-
<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>
|
|
836
|
-
<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>
|
|
837
|
-
<tr><td><code>?field_like</code></td><td><code>?name_like=ana</code></td><td>Case-insensitive substring match.</td></tr>
|
|
838
|
-
<tr><td><code>?field_start</code></td><td><code>?name_start=A</code></td><td>Case-insensitive prefix match.</td></tr>
|
|
839
|
-
<tr><td><code>?field_regex</code></td><td><code>?email_regex=gmail</code></td><td>Case-insensitive regular expression match.</td></tr>
|
|
840
|
-
<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>
|
|
841
|
-
<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>
|
|
842
|
-
<tr><td><code>?_page & ?_limit</code></td><td><code>?_page=2&_limit=10</code></td><td>${paginationDesc}</td></tr>
|
|
843
|
-
<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>
|
|
844
|
-
<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>
|
|
845
|
-
<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>
|
|
846
|
-
</tbody>
|
|
847
|
-
</table>
|
|
848
1547
|
</div>
|
|
849
1548
|
|
|
850
|
-
${collections.length ? `<h2>Examples</h2><div class="card">${examplesBlock(collections, relations, base, host, options, customRoutes[0])}</div>` : ""}
|
|
851
|
-
|
|
852
1549
|
<footer>
|
|
853
1550
|
Powered by <a href="https://github.com/aggiovato/yRest" target="_blank">@yrest/cli</a> \xB7 <a href="/_about">/_about</a>
|
|
854
1551
|
</footer>
|
|
855
1552
|
|
|
856
1553
|
</div>
|
|
857
1554
|
</body>
|
|
858
|
-
</html
|
|
1555
|
+
</html>`
|
|
1556
|
+
);
|
|
859
1557
|
}
|
|
860
1558
|
|
|
861
1559
|
// src/router/routes/about.routes.ts
|
|
@@ -1456,12 +2154,23 @@ var NestedRouteCommand = class {
|
|
|
1456
2154
|
relations;
|
|
1457
2155
|
base;
|
|
1458
2156
|
register(server) {
|
|
2157
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1459
2158
|
for (const [source, fields] of Object.entries(this.relations)) {
|
|
1460
2159
|
for (const [key, def] of Object.entries(fields)) {
|
|
1461
2160
|
if (def.type === "many2many") {
|
|
1462
|
-
this.
|
|
2161
|
+
const fwd = `${this.base}/${source}/:id/${key}`;
|
|
2162
|
+
const inv = `${this.base}/${def.target}/:id/${source}`;
|
|
2163
|
+
if (!registered.has(fwd) && !registered.has(inv)) {
|
|
2164
|
+
registered.add(fwd);
|
|
2165
|
+
registered.add(inv);
|
|
2166
|
+
this.registerMany2Many(server, source, key, def);
|
|
2167
|
+
}
|
|
1463
2168
|
} else {
|
|
1464
|
-
|
|
2169
|
+
const collPath = `${this.base}/${def.target}/:id/${source}`;
|
|
2170
|
+
if (!registered.has(collPath)) {
|
|
2171
|
+
registered.add(collPath);
|
|
2172
|
+
this.registerFkRelation(server, source, key, def.target, def.type);
|
|
2173
|
+
}
|
|
1465
2174
|
}
|
|
1466
2175
|
}
|
|
1467
2176
|
}
|