@yrest/cli 0.11.0 → 0.11.2

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/dist/cli/index.js CHANGED
@@ -717,6 +717,7 @@ function parseSchema(raw) {
717
717
  }
718
718
  return result;
719
719
  }
720
+ var VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
720
721
  function normaliseFieldDef(value) {
721
722
  if (value === "required") return { required: true };
722
723
  if (value === "optional") return { required: false };
@@ -724,15 +725,60 @@ function normaliseFieldDef(value) {
724
725
  const v = value;
725
726
  const def = {};
726
727
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
727
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
728
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
728
729
  def.type = v["_type"];
729
730
  if (typeof v["_format"] === "string") def.format = v["_format"];
730
731
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
731
732
  if (typeof v["_description"] === "string") def.description = v["_description"];
732
733
  if (v["_default"] !== void 0) def.default = v["_default"];
734
+ if (v["_example"] !== void 0) def.example = v["_example"];
735
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
736
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
737
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
738
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
739
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
740
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
741
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
742
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
743
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
744
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
745
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
746
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
747
+ def.uniqueItems = v["_uniqueItems"];
748
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
749
+ const items = v["_items"];
750
+ const itemsDef = {};
751
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
752
+ itemsDef.type = items["_type"];
753
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
754
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
755
+ def.items = itemsDef;
756
+ }
757
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
758
+ def.deprecated = v["_deprecated"];
759
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
760
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
761
+ def.writeOnly = v["_writeOnly"];
733
762
  return def;
734
763
  }
735
764
 
765
+ // src/storage/parseData.ts
766
+ var RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
767
+ function parseData(raw) {
768
+ const block = raw["_data"];
769
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
770
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
771
+ ) : {};
772
+ const fromRoot = Object.fromEntries(
773
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
774
+ );
775
+ return { ...fromBlock, ...fromRoot };
776
+ }
777
+ function hasDataBlock(raw) {
778
+ const d = raw["_data"];
779
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
780
+ }
781
+
736
782
  // src/utils/deepCopy.ts
737
783
  function deepCopyData(source) {
738
784
  return Object.fromEntries(
@@ -798,13 +844,11 @@ function serializeScenario(s) {
798
844
  function createYrestStorage(filePath) {
799
845
  const absPath = (0, import_node_path2.resolve)(filePath);
800
846
  const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
801
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
802
847
  const relations = parseRelations(raw["_rel"]);
803
848
  const routes = parseRoutes(raw["_routes"]);
804
849
  const schema = parseSchema(raw["_schema"]);
805
- const data = Object.fromEntries(
806
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
807
- );
850
+ const dataBlock = hasDataBlock(raw);
851
+ const data = parseData(raw);
808
852
  let snapshot = {
809
853
  data: deepCopyData(data),
810
854
  relations: { ...relations },
@@ -833,7 +877,11 @@ function createYrestStorage(filePath) {
833
877
  const payload = {};
834
878
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
835
879
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
836
- Object.assign(payload, data);
880
+ if (dataBlock) {
881
+ payload._data = data;
882
+ } else {
883
+ Object.assign(payload, data);
884
+ }
837
885
  const tmp = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(absPath), `.yrest-${(0, import_node_crypto.randomUUID)()}.tmp`);
838
886
  (0, import_node_fs2.writeFileSync)(tmp, (0, import_yaml.stringify)(payload), "utf8");
839
887
  (0, import_node_fs2.renameSync)(tmp, absPath);
@@ -841,9 +889,7 @@ function createYrestStorage(filePath) {
841
889
  reload() {
842
890
  const fresh = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
843
891
  const freshRelations = parseRelations(fresh["_rel"]);
844
- const freshData = Object.fromEntries(
845
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
846
- );
892
+ const freshData = parseData(fresh);
847
893
  for (const key of Object.keys(data)) delete data[key];
848
894
  Object.assign(data, freshData);
849
895
  for (const key of Object.keys(relations)) delete relations[key];
@@ -1223,9 +1269,15 @@ function generateERData(storage) {
1223
1269
  const relations = storage.getRelations();
1224
1270
  const schema = storage.getSchema();
1225
1271
  const pivotTables = /* @__PURE__ */ new Set();
1272
+ const pivotFKs = /* @__PURE__ */ new Map();
1226
1273
  for (const fields of Object.values(relations)) {
1227
1274
  for (const def of Object.values(fields)) {
1228
- if (def.type === "many2many") pivotTables.add(def.through);
1275
+ if (def.type === "many2many") {
1276
+ pivotTables.add(def.through);
1277
+ if (!pivotFKs.has(def.through)) pivotFKs.set(def.through, /* @__PURE__ */ new Set());
1278
+ pivotFKs.get(def.through).add(def.foreignKey);
1279
+ pivotFKs.get(def.through).add(def.otherKey);
1280
+ }
1229
1281
  }
1230
1282
  }
1231
1283
  const fkFields = /* @__PURE__ */ new Map();
@@ -1261,6 +1313,9 @@ function generateERData(storage) {
1261
1313
  for (const [source, fields] of Object.entries(relations)) {
1262
1314
  for (const [key, def] of Object.entries(fields)) {
1263
1315
  if (def.type === "many2many") {
1316
+ const throughRels = relations[def.through] ?? {};
1317
+ const fkRel = throughRels[def.foreignKey];
1318
+ const otherRel = throughRels[def.otherKey];
1264
1319
  erRelations.push({
1265
1320
  source,
1266
1321
  target: def.target,
@@ -1268,9 +1323,14 @@ function generateERData(storage) {
1268
1323
  label: def.through,
1269
1324
  through: def.through,
1270
1325
  foreignKey: def.foreignKey,
1271
- otherKey: def.otherKey
1326
+ otherKey: def.otherKey,
1327
+ fkCarDirect: fkRel?.carDirect,
1328
+ fkCarInverse: fkRel?.carInverse,
1329
+ otherCarDirect: otherRel?.carDirect,
1330
+ otherCarInverse: otherRel?.carInverse
1272
1331
  });
1273
1332
  } else {
1333
+ if (pivotFKs.get(source)?.has(key)) continue;
1274
1334
  erRelations.push({
1275
1335
  source,
1276
1336
  target: def.target,
@@ -1296,7 +1356,15 @@ function erDiagramSection(data) {
1296
1356
 
1297
1357
  <div class="card" style="padding:0;overflow:hidden;position:relative;">
1298
1358
  <canvas id="er-canvas" style="width:100%;display:block;cursor:grab;"></canvas>
1299
- <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>
1359
+ <div style="position:absolute;top:10px;right:14px;display:flex;gap:6px;align-items:center;">
1360
+ <select id="er-scale" title="Download quality" style="background:#161b22;border:1px solid #30363d;color:#7d8590;padding:3px 6px;border-radius:6px;font-size:11px;cursor:pointer;">
1361
+ <option value="1">1\xD7</option>
1362
+ <option value="2">2\xD7</option>
1363
+ <option value="3" selected>3\xD7</option>
1364
+ <option value="4">4\xD7</option>
1365
+ </select>
1366
+ <button id="er-dl" title="Download PNG" style="background:#161b22;border:1px solid #30363d;color:#7d8590;padding:4px 10px;border-radius:6px;font-size:11px;cursor:pointer;line-height:1.4;">\u2193 PNG</button>
1367
+ </div>
1300
1368
  </div>
1301
1369
 
1302
1370
  <script>${/*js*/
@@ -1306,7 +1374,7 @@ function erDiagramSection(data) {
1306
1374
  var D=${json};
1307
1375
 
1308
1376
  // \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
1309
- var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=8,BO=14,LGH=56;
1377
+ var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=12,BO=16,LGH=84,MIN_GAP=10;
1310
1378
 
1311
1379
  var C={bg:'#0d1117',nb:'#161b22',nh:'#1c2128',pb:'#0d1117',ph:'#161b22',
1312
1380
  bo:'#30363d',bs:'#58a6ff',tx:'#e6edf3',mu:'#7d8590',
@@ -1346,6 +1414,30 @@ function erDiagramSection(data) {
1346
1414
  }
1347
1415
  });
1348
1416
 
1417
+ // \u2500\u2500 Dynamic node width \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1418
+ function computeNodeWidths(){
1419
+ nodes.forEach(function(n){
1420
+ var e=n.e;
1421
+ ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
1422
+ var hdrW=FP+ctx.measureText(e.name).width+FP;
1423
+ if(e.isPivot){
1424
+ ctx.font='9px monospace';
1425
+ hdrW+=ctx.measureText('pivot').width+FP;
1426
+ }
1427
+ var maxRow=hdrW;
1428
+ e.fields.forEach(function(f){
1429
+ var leftPad=FP+(f.pk||f.fk?16:0);
1430
+ var rightPad=(f.required&&!f.pk?14:0)+FP;
1431
+ ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
1432
+ var nw=ctx.measureText(f.name).width;
1433
+ ctx.font='10px monospace';
1434
+ var tw=ctx.measureText(f.type).width;
1435
+ maxRow=Math.max(maxRow,leftPad+nw+MIN_GAP+tw+rightPad);
1436
+ });
1437
+ n.w=Math.max(NW,Math.ceil(maxRow));
1438
+ });
1439
+ }
1440
+
1349
1441
  // \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
1350
1442
  var groups=[];
1351
1443
 
@@ -1384,7 +1476,8 @@ function erDiagramSection(data) {
1384
1476
  median(adj[b].map(function(x){return np[x]!=null?np[x]:0;}));
1385
1477
  });
1386
1478
  }
1387
- var colW=NW+COL_GAP;
1479
+ var maxNW=nodes.reduce(function(m,n){return Math.max(m,n.w);},NW);
1480
+ var colW=maxNW+COL_GAP;
1388
1481
  var totalW=(maxL+1)*colW-COL_GAP;
1389
1482
  var startX=Math.max(20,(W-totalW)/2);
1390
1483
  for(var l=0;l<=maxL;l++){
@@ -1432,37 +1525,37 @@ function erDiagramSection(data) {
1432
1525
 
1433
1526
  var badges=[];
1434
1527
 
1435
- function pushBadge(x,y,ux,uy,many,col){
1436
- badges.push({x:x,y:y,ux:ux,uy:uy,many:many,col:col});
1528
+ // \u2500\u2500 SVG cardinality badges via Path2D \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1529
+ // SVG orientation: LEFT side touches the node, RIGHT side connects to the line.
1530
+ // Rotation = Math.atan2(uy,ux) aligns local +x with the outward (away-from-node) direction.
1531
+ var P2D={
1532
+ '1..1':new Path2D('M75 40C75 59.33 59.33 75 40 75C20.67 75 5 59.33 5 40C5 20.67 20.67 5 40 5C59.33 5 75 20.67 75 40ZM41 26C41 24.3431 39.6569 23 38 23C36.3431 23 35 24.3431 35 26V37H26V26C26 24.3431 24.6569 23 23 23C21.3431 23 20 24.3431 20 26V37H15C13.3431 37 12 38.3431 12 40C12 41.6569 13.3431 43 15 43H20V54C20 55.6569 21.3431 57 23 57C24.6569 57 26 55.6569 26 54V43H35V54C35 55.6569 36.3431 57 38 57C39.6569 57 41 55.6569 41 54V43H66C67.6569 43 69 41.6569 69 40C69 38.3431 67.6569 37 66 37H41V26Z'),
1533
+ '1..n':new Path2D('M40 75C59.33 75 75 59.33 75 40C75 20.67 59.33 5 40 5C20.67 5 5 20.67 5 40C5 59.33 20.67 75 40 75ZM44.9862 42.9025L18.6801 58.0904C17.2452 58.9188 15.4105 58.4272 14.582 56.9923C13.7536 55.5574 14.2452 53.7227 15.6801 52.8942L32.9862 42.9025H17C15.3431 42.9025 14 41.5594 14 39.9025C14 38.2457 15.3431 36.9025 17 36.9025H32.9862L15.5981 26.5986C14.1632 25.7702 13.6716 23.9354 14.5 22.5005C15.3284 21.0656 17.1632 20.574 18.5981 21.4024L44.9862 36.9025H46V26C46 24.3431 47.3431 23 49 23C50.6569 23 52 24.3431 52 26V36.9025H68C69.6569 36.9025 71 38.2457 71 39.9025C71 41.5594 69.6569 42.9025 68 42.9025H52V53C52 54.6569 50.6569 56 49 56C47.3431 56 46 54.6569 46 53V42.9025H44.9862Z'),
1534
+ '0..1':new Path2D('M75 40C75 59.33 59.33 75 40 75C20.67 75 5 59.33 5 40C5 20.67 20.67 5 40 5C59.33 5 75 20.67 75 40ZM23 57C24.6569 57 26 55.6569 26 54V43H33.3781C34.7102 48.1757 39.4085 52 45 52C50.5915 52 55.2898 48.1757 56.6219 43H66C67.6569 43 69 41.6569 69 40C69 38.3431 67.6569 37 66 37H56.6219C55.2898 31.8243 50.5915 28 45 28C39.4085 28 34.7102 31.8243 33.3781 37H26V26C26 24.3431 24.6569 23 23 23C21.3431 23 20 24.3431 20 26V37H15C13.3431 37 12 38.3431 12 40C12 41.6569 13.3431 43 15 43H20V54C20 55.6569 21.3431 57 23 57ZM51 40C51 41.0929 50.7078 42.1175 50.1973 43C49.1599 44.7934 47.2208 46 45 46C42.7792 46 40.8401 44.7934 39.8027 43C39.2922 42.1175 39 41.0929 39 40C39 38.9071 39.2922 37.8825 39.8027 37C40.8401 35.2066 42.7792 34 45 34C47.2208 34 49.1599 35.2066 50.1973 37C50.7078 37.8825 51 38.9071 51 40Z'),
1535
+ '0..n':new Path2D('M40 75C59.33 75 75 59.33 75 40C75 20.67 59.33 5 40 5C20.67 5 5 20.67 5 40C5 59.33 20.67 75 40 75ZM17 42.9025C15.3431 42.9025 14 41.5594 14 39.9025C14 38.2457 15.3431 36.9025 17 36.9025H32.9862L15.5981 26.5986C14.1632 25.7702 13.6716 23.9354 14.5 22.5005C15.3284 21.0656 17.1632 20.574 18.5981 21.4024L45.4091 36.8818L45.4036 36.9025H45.445L45.4091 36.8818C46.7819 31.7662 51.4511 28 57 28C62.579 28 67.2687 31.8072 68.6129 36.9652C69.9758 37.2481 71 38.4557 71 39.9025C71 41.331 70.0017 42.5262 68.6647 42.8286C67.3931 48.091 62.6531 52 57 52C51.3734 52 46.6513 48.1276 45.3534 42.9025H44.9862L18.6801 58.0904C17.2452 58.9188 15.4105 58.4272 14.582 56.9923C13.7536 55.5574 14.2452 53.7227 15.6801 52.8942L32.9862 42.9025H17ZM62.1397 36.9025C61.0891 35.163 59.1804 34 57 34C54.8196 34 52.9109 35.163 51.8603 36.9025C51.3142 37.8067 51 38.8667 51 40C51 41.0529 51.2712 42.0424 51.7475 42.9025C52.7704 44.7496 54.7392 46 57 46C59.2608 46 61.2296 44.7496 62.2525 42.9025C62.7288 42.0424 63 41.0529 63 40C63 38.8667 62.6858 37.8067 62.1397 36.9025Z'),
1536
+ 'n':new Path2D('M40 75C59.33 75 75 59.33 75 40C75 20.67 59.33 5 40 5C20.67 5 5 20.67 5 40C5 59.33 20.67 75 40 75ZM17 36.9025H33.0677L15.5981 26.5986C14.1632 25.7702 13.6716 23.9354 14.5 22.5005C15.3284 21.0656 17.1632 20.574 18.5981 21.4024L44.9862 36.9025H68C69.6569 36.9025 71 38.2457 71 39.9025C71 41.5594 69.6569 42.9025 68 42.9025H44.9862L18.6801 58.0904C17.2452 58.9188 15.4105 58.4272 14.582 56.9923C13.7536 55.5574 14.2452 53.7227 15.6801 52.8942L32.9862 42.9025H17C15.3431 42.9025 14 41.5594 14 39.9025C14 38.2457 15.3431 36.9025 17 36.9025Z')
1537
+ };
1538
+
1539
+ function pushBadge(x,y,ux,uy,car,col){
1540
+ badges.push({x:x,y:y,ux:ux,uy:uy,car:car,col:col});
1437
1541
  }
1438
1542
 
1439
- // Badge: solid colored circle; crow's foot (many) or double-bar (one) in dark ink inside.
1440
- // ux,uy points outward from the node \u2014 badge is offset BO px into the gap.
1441
- // Crow's foot tines open toward the node (-x in rotated frame), tip toward line (+x).
1442
1543
  function drawBadge(b){
1443
- var bx=b.x+b.ux*BO,by=b.y+b.uy*BO;
1444
- ctx.fillStyle=b.col;ctx.strokeStyle=b.col;ctx.lineWidth=1;
1445
- ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.stroke();
1446
- ctx.save();ctx.translate(bx,by);ctx.rotate(Math.atan2(b.uy,b.ux));
1447
- ctx.strokeStyle=C.bg;ctx.lineWidth=1.4;ctx.lineCap='round';
1448
- ctx.beginPath();
1449
- if(b.many){
1450
- ctx.moveTo(-5,-4);ctx.lineTo(5,0); // upper tine \u2192 tip
1451
- ctx.moveTo(-5,4);ctx.lineTo(5,0); // lower tine \u2192 tip
1452
- ctx.moveTo(-5,0);ctx.lineTo(5,0); // center line
1453
- ctx.moveTo(-5,-4);ctx.lineTo(-5,4); // base bar (near node)
1454
- }else{
1455
- ctx.moveTo(-3,-5);ctx.lineTo(-3,5); // near bar
1456
- ctx.moveTo(2,-5);ctx.lineTo(2,5); // far bar
1457
- }
1458
- ctx.stroke();
1544
+ var p=P2D[b.car]||P2D['n'],sc=BR/40;
1545
+ ctx.save();
1546
+ ctx.translate(b.x+b.ux*BO,b.y+b.uy*BO);
1547
+ ctx.rotate(Math.atan2(b.uy,b.ux));
1548
+ ctx.scale(sc,sc);
1549
+ ctx.translate(-40,-40);
1550
+ ctx.fillStyle=b.col;
1551
+ ctx.fill(p,'evenodd');
1459
1552
  ctx.restore();
1460
1553
  }
1461
1554
 
1462
1555
  function ecol(t){return t==='one2one'?C.o2o:t==='many2many'?C.m2m:C.m2o;}
1463
1556
 
1464
1557
  // offset: pixels to shift perpendicular to the edge direction (for parallel edges)
1465
- function drawEdgeLine(a,b,col,dashed,label,manyA,manyB,off){
1558
+ function drawEdgeLine(a,b,col,dashed,label,carA,carB,off){
1466
1559
  var p1=edgePt(a,b.x+b.w/2,b.y+b.h/2);
1467
1560
  var p2=edgePt(b,a.x+a.w/2,a.y+a.h/2);
1468
1561
  var dx=p2.x-p1.x,dy=p2.y-p1.y,d=Math.sqrt(dx*dx+dy*dy)||1;
@@ -1473,13 +1566,19 @@ function erDiagramSection(data) {
1473
1566
  ctx.setLineDash(dashed?[5,4]:[]);
1474
1567
  ctx.beginPath();ctx.moveTo(q1x,q1y);ctx.lineTo(q2x,q2y);ctx.stroke();
1475
1568
  ctx.setLineDash([]);
1476
- pushBadge(q1x,q1y,ux,uy,manyA,col);
1477
- pushBadge(q2x,q2y,-ux,-uy,manyB,col);
1569
+ pushBadge(q1x,q1y,ux,uy,carA,col);
1570
+ pushBadge(q2x,q2y,-ux,-uy,carB,col);
1478
1571
  if(label){
1479
1572
  var mx=(q1x+q2x)/2,my=(q1y+q2y)/2;
1573
+ var la=Math.atan2(uy,ux);
1574
+ if(la>Math.PI/2||la<-Math.PI/2)la+=Math.PI; // keep text readable
1575
+ var ls=off>0?11:-11; // alternate above/below for parallel edges
1576
+ ctx.save();
1577
+ ctx.translate(mx,my);
1578
+ ctx.rotate(la);
1480
1579
  ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
1481
- ctx.fillText(label,mx-uy*14,my+ux*14);
1482
- ctx.textBaseline='alphabetic';
1580
+ ctx.fillText(label,0,ls);
1581
+ ctx.restore();
1483
1582
  }
1484
1583
  }
1485
1584
 
@@ -1495,14 +1594,14 @@ function erDiagramSection(data) {
1495
1594
  var col=ecol(r.type);
1496
1595
  if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
1497
1596
  var sn=nodes[nIdx[r.source]],pn=nodes[nIdx[r.through]],tn=nodes[nIdx[r.target]];
1498
- if(sn&&pn)edges.push({a:sn,b:pn,col:col,dashed:true,label:r.foreignKey||'',manyA:false,manyB:true,off:0});
1499
- if(tn&&pn)edges.push({a:tn,b:pn,col:col,dashed:true,label:r.otherKey||'',manyA:false,manyB:true,off:0});
1597
+ if(sn&&pn)edges.push({a:sn,b:pn,col:col,dashed:true,label:r.foreignKey||'',carA:r.fkCarDirect||'1..1',carB:r.fkCarInverse||'0..n',off:0});
1598
+ if(tn&&pn)edges.push({a:tn,b:pn,col:col,dashed:true,label:r.otherKey||'',carA:r.otherCarDirect||'1..1',carB:r.otherCarInverse||'0..n',off:0});
1500
1599
  }else{
1501
1600
  var sn=nodes[nIdx[r.source]],tn=nodes[nIdx[r.target]];
1502
1601
  if(sn&&tn&&sn!==tn){
1503
- var manyA=r.carInverse?r.carInverse.endsWith('n'):r.type!=='one2one';
1504
- var manyB=r.carDirect?r.carDirect.endsWith('n'):r.type==='many2many';
1505
- edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,manyA:manyA,manyB:manyB,off:0});
1602
+ var carA=r.carInverse||(r.type!=='one2one'?'1..n':'1..1');
1603
+ var carB=r.carDirect||(r.type==='many2many'?'1..n':'1..1');
1604
+ edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,carA:carA,carB:carB,off:0});
1506
1605
  }
1507
1606
  }
1508
1607
  });
@@ -1561,12 +1660,24 @@ function erDiagramSection(data) {
1561
1660
 
1562
1661
  function drawLegend(){
1563
1662
  var cvW=cv.width/dpr,cvH=cv.height/dpr;
1564
- var y0=cvH-LGH,y1=cvH-LGH+18,y2=cvH-LGH+38;
1663
+ var yL=cvH-LGH,y1=yL+13,y2=yL+35,y3=yL+62;
1565
1664
 
1566
1665
  ctx.fillStyle='#161b22';
1567
- ctx.fillRect(0,y0,cvW,LGH);
1666
+ ctx.fillRect(0,yL,cvW,LGH);
1568
1667
  ctx.strokeStyle='#30363d';ctx.lineWidth=1;
1569
- ctx.beginPath();ctx.moveTo(0,y0);ctx.lineTo(cvW,y0);ctx.stroke();
1668
+ ctx.beginPath();ctx.moveTo(0,yL);ctx.lineTo(cvW,yL);ctx.stroke();
1669
+
1670
+ function legendBadge(cx,cy,car,col,angle){
1671
+ var p=P2D[car]||P2D['n'],sc=8/40;
1672
+ ctx.save();
1673
+ ctx.translate(cx,cy);
1674
+ ctx.rotate(angle);
1675
+ ctx.scale(sc,sc);
1676
+ ctx.translate(-40,-40);
1677
+ ctx.fillStyle=col;
1678
+ ctx.fill(p,'evenodd');
1679
+ ctx.restore();
1680
+ }
1570
1681
 
1571
1682
  // Row 1: relation line types
1572
1683
  var types=[
@@ -1585,42 +1696,43 @@ function erDiagramSection(data) {
1585
1696
  x+=18+ctx.measureText(it.l).width+22;
1586
1697
  });
1587
1698
 
1588
- // Row 2: cardinality symbols
1589
- function badge2(cx,cy,many,col){
1590
- ctx.fillStyle=col;ctx.strokeStyle=col;ctx.lineWidth=1;
1591
- ctx.beginPath();ctx.arc(cx,cy,6,0,Math.PI*2);ctx.fill();
1592
- ctx.save();ctx.translate(cx,cy);
1593
- ctx.strokeStyle=C.bg;ctx.lineWidth=1.2;ctx.lineCap='round';
1594
- ctx.beginPath();
1595
- if(many){
1596
- ctx.moveTo(-4,-3);ctx.lineTo(4,0);ctx.moveTo(-4,3);ctx.lineTo(4,0);
1597
- ctx.moveTo(-4,0);ctx.lineTo(4,0);ctx.moveTo(-4,-3);ctx.lineTo(-4,3);
1598
- }else{
1599
- ctx.moveTo(-2,-3);ctx.lineTo(-2,3);ctx.moveTo(2,-3);ctx.lineTo(2,3);
1600
- }
1601
- ctx.stroke();ctx.restore();
1602
- }
1603
-
1604
- ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1699
+ // Row 2: relation type examples (pair of badges + line)
1605
1700
  x=14;
1606
1701
  var pairs=[
1607
- {a:false,b:true,col:C.m2o,l:'many2one (N:1)'},
1608
- {a:false,b:false,col:C.o2o,l:'one2one (1:1)'},
1609
- {a:false,b:true,col:C.m2m,l:'pivot edge (1:N)'}
1702
+ {carA:'1..n',carB:'1..1',col:C.m2o,l:'many2one (N:1)'},
1703
+ {carA:'1..1',carB:'1..1',col:C.o2o,l:'one2one (1:1)'},
1704
+ {carA:'1..1',carB:'0..n',col:C.m2m,l:'pivot edge (1:0..N)'}
1610
1705
  ];
1611
1706
  pairs.forEach(function(p){
1612
1707
  ctx.strokeStyle=p.col+'55';ctx.lineWidth=1.2;ctx.setLineDash(p.col===C.m2m?[4,3]:[]);
1613
1708
  ctx.beginPath();ctx.moveTo(x+8,y2);ctx.lineTo(x+56,y2);ctx.stroke();
1614
1709
  ctx.setLineDash([]);
1615
- badge2(x+8,y2,p.a,p.col);
1616
- badge2(x+56,y2,p.b,p.col);
1710
+ legendBadge(x+8,y2,p.carA,p.col,0);
1711
+ legendBadge(x+56,y2,p.carB,p.col,Math.PI);
1617
1712
  ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1618
1713
  ctx.fillText(p.l,x+66,y2);
1619
1714
  x+=66+ctx.measureText(p.l).width+24;
1620
1715
  });
1621
1716
 
1717
+ // Row 3: individual symbol meanings
1718
+ var symbols=[
1719
+ {car:'1..1',l:'exactly one'},
1720
+ {car:'0..1',l:'zero or one'},
1721
+ {car:'1..n',l:'one or more'},
1722
+ {car:'0..n',l:'zero or more'},
1723
+ {car:'n', l:'many (pivot)'}
1724
+ ];
1725
+ x=14;
1726
+ ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1727
+ symbols.forEach(function(s){
1728
+ legendBadge(x+8,y3,s.car,C.mu,0);
1729
+ ctx.fillStyle=C.mu;
1730
+ ctx.fillText(s.l,x+20,y3);
1731
+ x+=20+ctx.measureText(s.l).width+18;
1732
+ });
1733
+
1622
1734
  ctx.fillStyle='#3d444d';ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
1623
- ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y2);
1735
+ ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y1);
1624
1736
  ctx.textBaseline='alphabetic';
1625
1737
  }
1626
1738
 
@@ -1632,7 +1744,7 @@ function erDiagramSection(data) {
1632
1744
  ctx.save();ctx.translate(pan.x,pan.y);ctx.scale(zm,zm);
1633
1745
  badges=[];
1634
1746
  var edges=collectEdges();
1635
- edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.manyA,e.manyB,e.off);});
1747
+ edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.carA,e.carB,e.off);});
1636
1748
  nodes.forEach(function(n){drawNode(n);});
1637
1749
  badges.forEach(function(b){drawBadge(b);});
1638
1750
  ctx.restore();
@@ -1678,14 +1790,21 @@ function erDiagramSection(data) {
1678
1790
  });
1679
1791
 
1680
1792
  document.getElementById('er-dl').addEventListener('click',function(){
1681
- var SCALE=3;
1682
- var cw=cv.width/dpr,ch=cv.height/dpr;
1793
+ var SCALE=parseInt(document.getElementById('er-scale').value,10)||3;
1794
+ var pad=36;
1795
+ var bx0=1e9,by0=1e9,bx1=-1e9,by1=-1e9;
1796
+ nodes.forEach(function(n){
1797
+ bx0=Math.min(bx0,n.x);by0=Math.min(by0,n.y);
1798
+ bx1=Math.max(bx1,n.x+n.w);by1=Math.max(by1,n.y+n.h);
1799
+ });
1800
+ var cw=bx1-bx0+pad*2,ch=by1-by0+pad*2+LGH;
1683
1801
  var oc=document.createElement('canvas');
1684
1802
  oc.width=Math.round(cw*SCALE);oc.height=Math.round(ch*SCALE);
1685
- var sav={cv:cv,ctx:ctx,dpr:dpr};
1803
+ var sav={cv:cv,ctx:ctx,dpr:dpr,px:pan.x,py:pan.y,zm:zm};
1686
1804
  cv=oc;ctx=oc.getContext('2d');dpr=SCALE;
1805
+ pan.x=pad-bx0;pan.y=pad-by0;zm=1;
1687
1806
  draw();
1688
- cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;
1807
+ cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;pan.x=sav.px;pan.y=sav.py;zm=sav.zm;
1689
1808
  var a=document.createElement('a');
1690
1809
  a.download='er-diagram.png';a.href=oc.toDataURL('image/png');a.click();
1691
1810
  });
@@ -1693,6 +1812,7 @@ function erDiagramSection(data) {
1693
1812
  // \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
1694
1813
  requestAnimationFrame(function(){
1695
1814
  resize();
1815
+ computeNodeWidths();
1696
1816
  var r=cv.getBoundingClientRect();
1697
1817
  computeLayout(r.width,r.height);
1698
1818
  var h=requiredH();
@@ -2329,15 +2449,19 @@ function resolveRequestPath(dotPath, req) {
2329
2449
  const [root, ...rest] = dotPath.split(".");
2330
2450
  let value;
2331
2451
  switch (root) {
2452
+ case "_body":
2332
2453
  case "body":
2333
2454
  value = req.body;
2334
2455
  break;
2456
+ case "_params":
2335
2457
  case "params":
2336
2458
  value = req.params;
2337
2459
  break;
2460
+ case "_query":
2338
2461
  case "query":
2339
2462
  value = req.query;
2340
2463
  break;
2464
+ case "_headers":
2341
2465
  case "headers":
2342
2466
  value = req.headers;
2343
2467
  break;
@@ -2629,6 +2753,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2629
2753
  if (def?.description) prop.description = def.description;
2630
2754
  if (def?.enum) prop.enum = def.enum;
2631
2755
  if (def?.default !== void 0) prop.default = def.default;
2756
+ if (def?.example !== void 0) prop.example = def.example;
2757
+ if (def?.nullable) prop.nullable = def.nullable;
2758
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2759
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2760
+ if (def?.pattern) prop.pattern = def.pattern;
2761
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2762
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2763
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2764
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2765
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2766
+ if (def?.items) prop.items = def.items;
2767
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2768
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2769
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2770
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2771
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2772
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2632
2773
  properties[field] = prop;
2633
2774
  if (def?.required === true) required.push(field);
2634
2775
  }
@@ -3171,6 +3312,14 @@ async function loadHandlers(filePath) {
3171
3312
  }
3172
3313
 
3173
3314
  // src/cli/commands/serve.ts
3315
+ function buildServeOptions(file, fileConfig, cliOverrides, fileArgIsExplicit = false) {
3316
+ return yrestOptionsSchema.parse({
3317
+ file,
3318
+ ...fileConfig,
3319
+ ...fileArgIsExplicit ? { file } : {},
3320
+ ...cliOverrides
3321
+ });
3322
+ }
3174
3323
  function registerServe(program2) {
3175
3324
  program2.command("serve").description("Start the mock server using a YAML file as database").argument("[file]", "Path to the YAML database file", "db.yml").option("-p, --port <number>", "Port to listen on", "3070").option("-H, --host <host>", "Host to bind", "localhost").option("-b, --base <path>", "Base path prefix for all routes", "").option("-w, --watch", "Reload db.yml automatically when it changes on disk").option("-r, --readonly", "Reject all write operations (POST, PUT, PATCH, DELETE) with 405").option(
3176
3325
  "-d, --delay <ms>",
@@ -3194,13 +3343,7 @@ function registerServe(program2) {
3194
3343
  const cliOverrides = Object.fromEntries(
3195
3344
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3196
3345
  );
3197
- const merged = {
3198
- file,
3199
- ...fileConfig,
3200
- ...cmd.args.length > 0 ? { file } : {},
3201
- ...cliOverrides
3202
- };
3203
- const options = yrestOptionsSchema.parse(merged);
3346
+ const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3204
3347
  let storage;
3205
3348
  try {
3206
3349
  storage = createYrestStorage(options.file);