@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.
@@ -690,6 +690,7 @@ function parseSchema(raw) {
690
690
  }
691
691
  return result;
692
692
  }
693
+ var VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
693
694
  function normaliseFieldDef(value) {
694
695
  if (value === "required") return { required: true };
695
696
  if (value === "optional") return { required: false };
@@ -697,15 +698,60 @@ function normaliseFieldDef(value) {
697
698
  const v = value;
698
699
  const def = {};
699
700
  if (v["_required"] === true || v["_required"] === false) def.required = v["_required"];
700
- if (typeof v["_type"] === "string" && ["string", "integer", "number", "boolean", "object", "array"].includes(v["_type"]))
701
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
701
702
  def.type = v["_type"];
702
703
  if (typeof v["_format"] === "string") def.format = v["_format"];
703
704
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
704
705
  if (typeof v["_description"] === "string") def.description = v["_description"];
705
706
  if (v["_default"] !== void 0) def.default = v["_default"];
707
+ if (v["_example"] !== void 0) def.example = v["_example"];
708
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
709
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
710
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
711
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
712
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
713
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
714
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
715
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
716
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
717
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
718
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
719
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
720
+ def.uniqueItems = v["_uniqueItems"];
721
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
722
+ const items = v["_items"];
723
+ const itemsDef = {};
724
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
725
+ itemsDef.type = items["_type"];
726
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
727
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
728
+ def.items = itemsDef;
729
+ }
730
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
731
+ def.deprecated = v["_deprecated"];
732
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
733
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
734
+ def.writeOnly = v["_writeOnly"];
706
735
  return def;
707
736
  }
708
737
 
738
+ // src/storage/parseData.ts
739
+ var RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
740
+ function parseData(raw) {
741
+ const block = raw["_data"];
742
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
743
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
744
+ ) : {};
745
+ const fromRoot = Object.fromEntries(
746
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
747
+ );
748
+ return { ...fromBlock, ...fromRoot };
749
+ }
750
+ function hasDataBlock(raw) {
751
+ const d = raw["_data"];
752
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
753
+ }
754
+
709
755
  // src/utils/deepCopy.ts
710
756
  function deepCopyData(source) {
711
757
  return Object.fromEntries(
@@ -771,13 +817,11 @@ function serializeScenario(s) {
771
817
  function createYrestStorage(filePath) {
772
818
  const absPath = resolve2(filePath);
773
819
  const raw = parse(readFileSync(absPath, "utf8")) ?? {};
774
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
775
820
  const relations = parseRelations(raw["_rel"]);
776
821
  const routes = parseRoutes(raw["_routes"]);
777
822
  const schema = parseSchema(raw["_schema"]);
778
- const data = Object.fromEntries(
779
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
780
- );
823
+ const dataBlock = hasDataBlock(raw);
824
+ const data = parseData(raw);
781
825
  let snapshot = {
782
826
  data: deepCopyData(data),
783
827
  relations: { ...relations },
@@ -806,7 +850,11 @@ function createYrestStorage(filePath) {
806
850
  const payload = {};
807
851
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
808
852
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
809
- Object.assign(payload, data);
853
+ if (dataBlock) {
854
+ payload._data = data;
855
+ } else {
856
+ Object.assign(payload, data);
857
+ }
810
858
  const tmp = resolve2(dirname(absPath), `.yrest-${randomUUID()}.tmp`);
811
859
  writeFileSync2(tmp, stringify(payload), "utf8");
812
860
  renameSync(tmp, absPath);
@@ -814,9 +862,7 @@ function createYrestStorage(filePath) {
814
862
  reload() {
815
863
  const fresh = parse(readFileSync(absPath, "utf8")) ?? {};
816
864
  const freshRelations = parseRelations(fresh["_rel"]);
817
- const freshData = Object.fromEntries(
818
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
819
- );
865
+ const freshData = parseData(fresh);
820
866
  for (const key of Object.keys(data)) delete data[key];
821
867
  Object.assign(data, freshData);
822
868
  for (const key of Object.keys(relations)) delete relations[key];
@@ -1196,9 +1242,15 @@ function generateERData(storage) {
1196
1242
  const relations = storage.getRelations();
1197
1243
  const schema = storage.getSchema();
1198
1244
  const pivotTables = /* @__PURE__ */ new Set();
1245
+ const pivotFKs = /* @__PURE__ */ new Map();
1199
1246
  for (const fields of Object.values(relations)) {
1200
1247
  for (const def of Object.values(fields)) {
1201
- if (def.type === "many2many") pivotTables.add(def.through);
1248
+ if (def.type === "many2many") {
1249
+ pivotTables.add(def.through);
1250
+ if (!pivotFKs.has(def.through)) pivotFKs.set(def.through, /* @__PURE__ */ new Set());
1251
+ pivotFKs.get(def.through).add(def.foreignKey);
1252
+ pivotFKs.get(def.through).add(def.otherKey);
1253
+ }
1202
1254
  }
1203
1255
  }
1204
1256
  const fkFields = /* @__PURE__ */ new Map();
@@ -1234,6 +1286,9 @@ function generateERData(storage) {
1234
1286
  for (const [source, fields] of Object.entries(relations)) {
1235
1287
  for (const [key, def] of Object.entries(fields)) {
1236
1288
  if (def.type === "many2many") {
1289
+ const throughRels = relations[def.through] ?? {};
1290
+ const fkRel = throughRels[def.foreignKey];
1291
+ const otherRel = throughRels[def.otherKey];
1237
1292
  erRelations.push({
1238
1293
  source,
1239
1294
  target: def.target,
@@ -1241,9 +1296,14 @@ function generateERData(storage) {
1241
1296
  label: def.through,
1242
1297
  through: def.through,
1243
1298
  foreignKey: def.foreignKey,
1244
- otherKey: def.otherKey
1299
+ otherKey: def.otherKey,
1300
+ fkCarDirect: fkRel?.carDirect,
1301
+ fkCarInverse: fkRel?.carInverse,
1302
+ otherCarDirect: otherRel?.carDirect,
1303
+ otherCarInverse: otherRel?.carInverse
1245
1304
  });
1246
1305
  } else {
1306
+ if (pivotFKs.get(source)?.has(key)) continue;
1247
1307
  erRelations.push({
1248
1308
  source,
1249
1309
  target: def.target,
@@ -1269,7 +1329,15 @@ function erDiagramSection(data) {
1269
1329
 
1270
1330
  <div class="card" style="padding:0;overflow:hidden;position:relative;">
1271
1331
  <canvas id="er-canvas" style="width:100%;display:block;cursor:grab;"></canvas>
1272
- <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>
1332
+ <div style="position:absolute;top:10px;right:14px;display:flex;gap:6px;align-items:center;">
1333
+ <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;">
1334
+ <option value="1">1\xD7</option>
1335
+ <option value="2">2\xD7</option>
1336
+ <option value="3" selected>3\xD7</option>
1337
+ <option value="4">4\xD7</option>
1338
+ </select>
1339
+ <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>
1340
+ </div>
1273
1341
  </div>
1274
1342
 
1275
1343
  <script>${/*js*/
@@ -1279,7 +1347,7 @@ function erDiagramSection(data) {
1279
1347
  var D=${json};
1280
1348
 
1281
1349
  // \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
1282
- var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=8,BO=14,LGH=56;
1350
+ var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=12,BO=16,LGH=84,MIN_GAP=10;
1283
1351
 
1284
1352
  var C={bg:'#0d1117',nb:'#161b22',nh:'#1c2128',pb:'#0d1117',ph:'#161b22',
1285
1353
  bo:'#30363d',bs:'#58a6ff',tx:'#e6edf3',mu:'#7d8590',
@@ -1319,6 +1387,30 @@ function erDiagramSection(data) {
1319
1387
  }
1320
1388
  });
1321
1389
 
1390
+ // \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
1391
+ function computeNodeWidths(){
1392
+ nodes.forEach(function(n){
1393
+ var e=n.e;
1394
+ ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
1395
+ var hdrW=FP+ctx.measureText(e.name).width+FP;
1396
+ if(e.isPivot){
1397
+ ctx.font='9px monospace';
1398
+ hdrW+=ctx.measureText('pivot').width+FP;
1399
+ }
1400
+ var maxRow=hdrW;
1401
+ e.fields.forEach(function(f){
1402
+ var leftPad=FP+(f.pk||f.fk?16:0);
1403
+ var rightPad=(f.required&&!f.pk?14:0)+FP;
1404
+ ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
1405
+ var nw=ctx.measureText(f.name).width;
1406
+ ctx.font='10px monospace';
1407
+ var tw=ctx.measureText(f.type).width;
1408
+ maxRow=Math.max(maxRow,leftPad+nw+MIN_GAP+tw+rightPad);
1409
+ });
1410
+ n.w=Math.max(NW,Math.ceil(maxRow));
1411
+ });
1412
+ }
1413
+
1322
1414
  // \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
1323
1415
  var groups=[];
1324
1416
 
@@ -1357,7 +1449,8 @@ function erDiagramSection(data) {
1357
1449
  median(adj[b].map(function(x){return np[x]!=null?np[x]:0;}));
1358
1450
  });
1359
1451
  }
1360
- var colW=NW+COL_GAP;
1452
+ var maxNW=nodes.reduce(function(m,n){return Math.max(m,n.w);},NW);
1453
+ var colW=maxNW+COL_GAP;
1361
1454
  var totalW=(maxL+1)*colW-COL_GAP;
1362
1455
  var startX=Math.max(20,(W-totalW)/2);
1363
1456
  for(var l=0;l<=maxL;l++){
@@ -1405,37 +1498,37 @@ function erDiagramSection(data) {
1405
1498
 
1406
1499
  var badges=[];
1407
1500
 
1408
- function pushBadge(x,y,ux,uy,many,col){
1409
- badges.push({x:x,y:y,ux:ux,uy:uy,many:many,col:col});
1501
+ // \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
1502
+ // SVG orientation: LEFT side touches the node, RIGHT side connects to the line.
1503
+ // Rotation = Math.atan2(uy,ux) aligns local +x with the outward (away-from-node) direction.
1504
+ var P2D={
1505
+ '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'),
1506
+ '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'),
1507
+ '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'),
1508
+ '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'),
1509
+ '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')
1510
+ };
1511
+
1512
+ function pushBadge(x,y,ux,uy,car,col){
1513
+ badges.push({x:x,y:y,ux:ux,uy:uy,car:car,col:col});
1410
1514
  }
1411
1515
 
1412
- // Badge: solid colored circle; crow's foot (many) or double-bar (one) in dark ink inside.
1413
- // ux,uy points outward from the node \u2014 badge is offset BO px into the gap.
1414
- // Crow's foot tines open toward the node (-x in rotated frame), tip toward line (+x).
1415
1516
  function drawBadge(b){
1416
- var bx=b.x+b.ux*BO,by=b.y+b.uy*BO;
1417
- ctx.fillStyle=b.col;ctx.strokeStyle=b.col;ctx.lineWidth=1;
1418
- ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.stroke();
1419
- ctx.save();ctx.translate(bx,by);ctx.rotate(Math.atan2(b.uy,b.ux));
1420
- ctx.strokeStyle=C.bg;ctx.lineWidth=1.4;ctx.lineCap='round';
1421
- ctx.beginPath();
1422
- if(b.many){
1423
- ctx.moveTo(-5,-4);ctx.lineTo(5,0); // upper tine \u2192 tip
1424
- ctx.moveTo(-5,4);ctx.lineTo(5,0); // lower tine \u2192 tip
1425
- ctx.moveTo(-5,0);ctx.lineTo(5,0); // center line
1426
- ctx.moveTo(-5,-4);ctx.lineTo(-5,4); // base bar (near node)
1427
- }else{
1428
- ctx.moveTo(-3,-5);ctx.lineTo(-3,5); // near bar
1429
- ctx.moveTo(2,-5);ctx.lineTo(2,5); // far bar
1430
- }
1431
- ctx.stroke();
1517
+ var p=P2D[b.car]||P2D['n'],sc=BR/40;
1518
+ ctx.save();
1519
+ ctx.translate(b.x+b.ux*BO,b.y+b.uy*BO);
1520
+ ctx.rotate(Math.atan2(b.uy,b.ux));
1521
+ ctx.scale(sc,sc);
1522
+ ctx.translate(-40,-40);
1523
+ ctx.fillStyle=b.col;
1524
+ ctx.fill(p,'evenodd');
1432
1525
  ctx.restore();
1433
1526
  }
1434
1527
 
1435
1528
  function ecol(t){return t==='one2one'?C.o2o:t==='many2many'?C.m2m:C.m2o;}
1436
1529
 
1437
1530
  // offset: pixels to shift perpendicular to the edge direction (for parallel edges)
1438
- function drawEdgeLine(a,b,col,dashed,label,manyA,manyB,off){
1531
+ function drawEdgeLine(a,b,col,dashed,label,carA,carB,off){
1439
1532
  var p1=edgePt(a,b.x+b.w/2,b.y+b.h/2);
1440
1533
  var p2=edgePt(b,a.x+a.w/2,a.y+a.h/2);
1441
1534
  var dx=p2.x-p1.x,dy=p2.y-p1.y,d=Math.sqrt(dx*dx+dy*dy)||1;
@@ -1446,13 +1539,19 @@ function erDiagramSection(data) {
1446
1539
  ctx.setLineDash(dashed?[5,4]:[]);
1447
1540
  ctx.beginPath();ctx.moveTo(q1x,q1y);ctx.lineTo(q2x,q2y);ctx.stroke();
1448
1541
  ctx.setLineDash([]);
1449
- pushBadge(q1x,q1y,ux,uy,manyA,col);
1450
- pushBadge(q2x,q2y,-ux,-uy,manyB,col);
1542
+ pushBadge(q1x,q1y,ux,uy,carA,col);
1543
+ pushBadge(q2x,q2y,-ux,-uy,carB,col);
1451
1544
  if(label){
1452
1545
  var mx=(q1x+q2x)/2,my=(q1y+q2y)/2;
1546
+ var la=Math.atan2(uy,ux);
1547
+ if(la>Math.PI/2||la<-Math.PI/2)la+=Math.PI; // keep text readable
1548
+ var ls=off>0?11:-11; // alternate above/below for parallel edges
1549
+ ctx.save();
1550
+ ctx.translate(mx,my);
1551
+ ctx.rotate(la);
1453
1552
  ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='center';ctx.textBaseline='middle';
1454
- ctx.fillText(label,mx-uy*14,my+ux*14);
1455
- ctx.textBaseline='alphabetic';
1553
+ ctx.fillText(label,0,ls);
1554
+ ctx.restore();
1456
1555
  }
1457
1556
  }
1458
1557
 
@@ -1468,14 +1567,14 @@ function erDiagramSection(data) {
1468
1567
  var col=ecol(r.type);
1469
1568
  if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
1470
1569
  var sn=nodes[nIdx[r.source]],pn=nodes[nIdx[r.through]],tn=nodes[nIdx[r.target]];
1471
- if(sn&&pn)edges.push({a:sn,b:pn,col:col,dashed:true,label:r.foreignKey||'',manyA:false,manyB:true,off:0});
1472
- if(tn&&pn)edges.push({a:tn,b:pn,col:col,dashed:true,label:r.otherKey||'',manyA:false,manyB:true,off:0});
1570
+ 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});
1571
+ 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});
1473
1572
  }else{
1474
1573
  var sn=nodes[nIdx[r.source]],tn=nodes[nIdx[r.target]];
1475
1574
  if(sn&&tn&&sn!==tn){
1476
- var manyA=r.carInverse?r.carInverse.endsWith('n'):r.type!=='one2one';
1477
- var manyB=r.carDirect?r.carDirect.endsWith('n'):r.type==='many2many';
1478
- edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,manyA:manyA,manyB:manyB,off:0});
1575
+ var carA=r.carInverse||(r.type!=='one2one'?'1..n':'1..1');
1576
+ var carB=r.carDirect||(r.type==='many2many'?'1..n':'1..1');
1577
+ edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,carA:carA,carB:carB,off:0});
1479
1578
  }
1480
1579
  }
1481
1580
  });
@@ -1534,12 +1633,24 @@ function erDiagramSection(data) {
1534
1633
 
1535
1634
  function drawLegend(){
1536
1635
  var cvW=cv.width/dpr,cvH=cv.height/dpr;
1537
- var y0=cvH-LGH,y1=cvH-LGH+18,y2=cvH-LGH+38;
1636
+ var yL=cvH-LGH,y1=yL+13,y2=yL+35,y3=yL+62;
1538
1637
 
1539
1638
  ctx.fillStyle='#161b22';
1540
- ctx.fillRect(0,y0,cvW,LGH);
1639
+ ctx.fillRect(0,yL,cvW,LGH);
1541
1640
  ctx.strokeStyle='#30363d';ctx.lineWidth=1;
1542
- ctx.beginPath();ctx.moveTo(0,y0);ctx.lineTo(cvW,y0);ctx.stroke();
1641
+ ctx.beginPath();ctx.moveTo(0,yL);ctx.lineTo(cvW,yL);ctx.stroke();
1642
+
1643
+ function legendBadge(cx,cy,car,col,angle){
1644
+ var p=P2D[car]||P2D['n'],sc=8/40;
1645
+ ctx.save();
1646
+ ctx.translate(cx,cy);
1647
+ ctx.rotate(angle);
1648
+ ctx.scale(sc,sc);
1649
+ ctx.translate(-40,-40);
1650
+ ctx.fillStyle=col;
1651
+ ctx.fill(p,'evenodd');
1652
+ ctx.restore();
1653
+ }
1543
1654
 
1544
1655
  // Row 1: relation line types
1545
1656
  var types=[
@@ -1558,42 +1669,43 @@ function erDiagramSection(data) {
1558
1669
  x+=18+ctx.measureText(it.l).width+22;
1559
1670
  });
1560
1671
 
1561
- // Row 2: cardinality symbols
1562
- function badge2(cx,cy,many,col){
1563
- ctx.fillStyle=col;ctx.strokeStyle=col;ctx.lineWidth=1;
1564
- ctx.beginPath();ctx.arc(cx,cy,6,0,Math.PI*2);ctx.fill();
1565
- ctx.save();ctx.translate(cx,cy);
1566
- ctx.strokeStyle=C.bg;ctx.lineWidth=1.2;ctx.lineCap='round';
1567
- ctx.beginPath();
1568
- if(many){
1569
- ctx.moveTo(-4,-3);ctx.lineTo(4,0);ctx.moveTo(-4,3);ctx.lineTo(4,0);
1570
- ctx.moveTo(-4,0);ctx.lineTo(4,0);ctx.moveTo(-4,-3);ctx.lineTo(-4,3);
1571
- }else{
1572
- ctx.moveTo(-2,-3);ctx.lineTo(-2,3);ctx.moveTo(2,-3);ctx.lineTo(2,3);
1573
- }
1574
- ctx.stroke();ctx.restore();
1575
- }
1576
-
1577
- ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1672
+ // Row 2: relation type examples (pair of badges + line)
1578
1673
  x=14;
1579
1674
  var pairs=[
1580
- {a:false,b:true,col:C.m2o,l:'many2one (N:1)'},
1581
- {a:false,b:false,col:C.o2o,l:'one2one (1:1)'},
1582
- {a:false,b:true,col:C.m2m,l:'pivot edge (1:N)'}
1675
+ {carA:'1..n',carB:'1..1',col:C.m2o,l:'many2one (N:1)'},
1676
+ {carA:'1..1',carB:'1..1',col:C.o2o,l:'one2one (1:1)'},
1677
+ {carA:'1..1',carB:'0..n',col:C.m2m,l:'pivot edge (1:0..N)'}
1583
1678
  ];
1584
1679
  pairs.forEach(function(p){
1585
1680
  ctx.strokeStyle=p.col+'55';ctx.lineWidth=1.2;ctx.setLineDash(p.col===C.m2m?[4,3]:[]);
1586
1681
  ctx.beginPath();ctx.moveTo(x+8,y2);ctx.lineTo(x+56,y2);ctx.stroke();
1587
1682
  ctx.setLineDash([]);
1588
- badge2(x+8,y2,p.a,p.col);
1589
- badge2(x+56,y2,p.b,p.col);
1683
+ legendBadge(x+8,y2,p.carA,p.col,0);
1684
+ legendBadge(x+56,y2,p.carB,p.col,Math.PI);
1590
1685
  ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1591
1686
  ctx.fillText(p.l,x+66,y2);
1592
1687
  x+=66+ctx.measureText(p.l).width+24;
1593
1688
  });
1594
1689
 
1690
+ // Row 3: individual symbol meanings
1691
+ var symbols=[
1692
+ {car:'1..1',l:'exactly one'},
1693
+ {car:'0..1',l:'zero or one'},
1694
+ {car:'1..n',l:'one or more'},
1695
+ {car:'0..n',l:'zero or more'},
1696
+ {car:'n', l:'many (pivot)'}
1697
+ ];
1698
+ x=14;
1699
+ ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1700
+ symbols.forEach(function(s){
1701
+ legendBadge(x+8,y3,s.car,C.mu,0);
1702
+ ctx.fillStyle=C.mu;
1703
+ ctx.fillText(s.l,x+20,y3);
1704
+ x+=20+ctx.measureText(s.l).width+18;
1705
+ });
1706
+
1595
1707
  ctx.fillStyle='#3d444d';ctx.font='10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';
1596
- ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y2);
1708
+ ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y1);
1597
1709
  ctx.textBaseline='alphabetic';
1598
1710
  }
1599
1711
 
@@ -1605,7 +1717,7 @@ function erDiagramSection(data) {
1605
1717
  ctx.save();ctx.translate(pan.x,pan.y);ctx.scale(zm,zm);
1606
1718
  badges=[];
1607
1719
  var edges=collectEdges();
1608
- edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.manyA,e.manyB,e.off);});
1720
+ edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.carA,e.carB,e.off);});
1609
1721
  nodes.forEach(function(n){drawNode(n);});
1610
1722
  badges.forEach(function(b){drawBadge(b);});
1611
1723
  ctx.restore();
@@ -1651,14 +1763,21 @@ function erDiagramSection(data) {
1651
1763
  });
1652
1764
 
1653
1765
  document.getElementById('er-dl').addEventListener('click',function(){
1654
- var SCALE=3;
1655
- var cw=cv.width/dpr,ch=cv.height/dpr;
1766
+ var SCALE=parseInt(document.getElementById('er-scale').value,10)||3;
1767
+ var pad=36;
1768
+ var bx0=1e9,by0=1e9,bx1=-1e9,by1=-1e9;
1769
+ nodes.forEach(function(n){
1770
+ bx0=Math.min(bx0,n.x);by0=Math.min(by0,n.y);
1771
+ bx1=Math.max(bx1,n.x+n.w);by1=Math.max(by1,n.y+n.h);
1772
+ });
1773
+ var cw=bx1-bx0+pad*2,ch=by1-by0+pad*2+LGH;
1656
1774
  var oc=document.createElement('canvas');
1657
1775
  oc.width=Math.round(cw*SCALE);oc.height=Math.round(ch*SCALE);
1658
- var sav={cv:cv,ctx:ctx,dpr:dpr};
1776
+ var sav={cv:cv,ctx:ctx,dpr:dpr,px:pan.x,py:pan.y,zm:zm};
1659
1777
  cv=oc;ctx=oc.getContext('2d');dpr=SCALE;
1778
+ pan.x=pad-bx0;pan.y=pad-by0;zm=1;
1660
1779
  draw();
1661
- cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;
1780
+ cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;pan.x=sav.px;pan.y=sav.py;zm=sav.zm;
1662
1781
  var a=document.createElement('a');
1663
1782
  a.download='er-diagram.png';a.href=oc.toDataURL('image/png');a.click();
1664
1783
  });
@@ -1666,6 +1785,7 @@ function erDiagramSection(data) {
1666
1785
  // \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
1667
1786
  requestAnimationFrame(function(){
1668
1787
  resize();
1788
+ computeNodeWidths();
1669
1789
  var r=cv.getBoundingClientRect();
1670
1790
  computeLayout(r.width,r.height);
1671
1791
  var h=requiredH();
@@ -2302,15 +2422,19 @@ function resolveRequestPath(dotPath, req) {
2302
2422
  const [root, ...rest] = dotPath.split(".");
2303
2423
  let value;
2304
2424
  switch (root) {
2425
+ case "_body":
2305
2426
  case "body":
2306
2427
  value = req.body;
2307
2428
  break;
2429
+ case "_params":
2308
2430
  case "params":
2309
2431
  value = req.params;
2310
2432
  break;
2433
+ case "_query":
2311
2434
  case "query":
2312
2435
  value = req.query;
2313
2436
  break;
2437
+ case "_headers":
2314
2438
  case "headers":
2315
2439
  value = req.headers;
2316
2440
  break;
@@ -2602,6 +2726,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2602
2726
  if (def?.description) prop.description = def.description;
2603
2727
  if (def?.enum) prop.enum = def.enum;
2604
2728
  if (def?.default !== void 0) prop.default = def.default;
2729
+ if (def?.example !== void 0) prop.example = def.example;
2730
+ if (def?.nullable) prop.nullable = def.nullable;
2731
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2732
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2733
+ if (def?.pattern) prop.pattern = def.pattern;
2734
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2735
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2736
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2737
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2738
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2739
+ if (def?.items) prop.items = def.items;
2740
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2741
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2742
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2743
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2744
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2745
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2605
2746
  properties[field] = prop;
2606
2747
  if (def?.required === true) required.push(field);
2607
2748
  }
@@ -3144,6 +3285,14 @@ async function loadHandlers(filePath) {
3144
3285
  }
3145
3286
 
3146
3287
  // src/cli/commands/serve.ts
3288
+ function buildServeOptions(file, fileConfig, cliOverrides, fileArgIsExplicit = false) {
3289
+ return yrestOptionsSchema.parse({
3290
+ file,
3291
+ ...fileConfig,
3292
+ ...fileArgIsExplicit ? { file } : {},
3293
+ ...cliOverrides
3294
+ });
3295
+ }
3147
3296
  function registerServe(program2) {
3148
3297
  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(
3149
3298
  "-d, --delay <ms>",
@@ -3167,13 +3316,7 @@ function registerServe(program2) {
3167
3316
  const cliOverrides = Object.fromEntries(
3168
3317
  Object.entries(flags).filter(([key]) => cmd.getOptionValueSource(key) === "cli")
3169
3318
  );
3170
- const merged = {
3171
- file,
3172
- ...fileConfig,
3173
- ...cmd.args.length > 0 ? { file } : {},
3174
- ...cliOverrides
3175
- };
3176
- const options = yrestOptionsSchema.parse(merged);
3319
+ const options = buildServeOptions(file, fileConfig, cliOverrides, cmd.args.length > 0);
3177
3320
  let storage;
3178
3321
  try {
3179
3322
  storage = createYrestStorage(options.file);
package/dist/index.d.mts CHANGED
@@ -10,21 +10,87 @@ type Resource = Record<string, unknown>;
10
10
  * All properties are optional — omit what you don't need. Fields not mentioned
11
11
  * in `_schema` are inferred from the collection data and treated as optional.
12
12
  *
13
+ * In the YAML file every property uses the `_` prefix convention:
14
+ * `_type`, `_required`, `_format`, `_enum`, `_description`, `_default`, etc.
15
+ *
13
16
  * Shorthand: `fieldName: required` normalises to `{ required: true }`.
17
+ *
18
+ * @example
19
+ * ```yaml
20
+ * _schema:
21
+ * users:
22
+ * name:
23
+ * _type: string
24
+ * _required: true
25
+ * _minLength: 2
26
+ * _maxLength: 100
27
+ * email:
28
+ * _type: string
29
+ * _required: true
30
+ * _format: email
31
+ * age:
32
+ * _type: integer
33
+ * _minimum: 0
34
+ * _maximum: 120
35
+ * _nullable: true
36
+ * role:
37
+ * _type: string
38
+ * _enum: [admin, editor, viewer]
39
+ * _default: viewer
40
+ * tags:
41
+ * _type: array
42
+ * _items:
43
+ * _type: string
44
+ * _uniqueItems: true
45
+ * ```
14
46
  */
15
47
  type FieldDef = {
16
- /** If `true`, the field is listed in the OpenAPI `required` array and (Phase B) validated on POST/PUT. */
48
+ /** If `true`, the field is listed in the OpenAPI `required` array. */
17
49
  required?: boolean;
18
50
  /** Explicit type override. If absent, inferred from the data. */
19
51
  type?: "string" | "integer" | "number" | "boolean" | "object" | "array";
20
- /** OpenAPI `format` hint (e.g. `email`, `date`, `uuid`, `uri`, `date-time`). */
52
+ /** OpenAPI `format` hint (e.g. `email`, `date`, `uuid`, `uri`, `date-time`, `binary`). */
21
53
  format?: string;
22
- /** Restricts the field to a fixed set of values. */
54
+ /** Restricts the field to a fixed set of allowed values. */
23
55
  enum?: unknown[];
24
56
  /** Human-readable description included in the OpenAPI spec. */
25
57
  description?: string;
26
58
  /** Default value included in the OpenAPI spec. */
27
59
  default?: unknown;
60
+ /** Example value included in the OpenAPI spec. */
61
+ example?: unknown;
62
+ /** If `true`, the field may be `null` in addition to its declared type. */
63
+ nullable?: boolean;
64
+ /** Minimum string length (inclusive). Applies when `type` is `string`. */
65
+ minLength?: number;
66
+ /** Maximum string length (inclusive). Applies when `type` is `string`. */
67
+ maxLength?: number;
68
+ /** Regex pattern the string value must match. Applies when `type` is `string`. */
69
+ pattern?: string;
70
+ /** Minimum numeric value (inclusive). Applies when `type` is `number` or `integer`. */
71
+ minimum?: number;
72
+ /** Maximum numeric value (inclusive). Applies when `type` is `number` or `integer`. */
73
+ maximum?: number;
74
+ /** Exclusive lower bound. Applies when `type` is `number` or `integer`. */
75
+ exclusiveMinimum?: number;
76
+ /** Exclusive upper bound. Applies when `type` is `number` or `integer`. */
77
+ exclusiveMaximum?: number;
78
+ /** Value must be a multiple of this number. */
79
+ multipleOf?: number;
80
+ /** Schema descriptor for the items of an array field. */
81
+ items?: Pick<FieldDef, "type" | "format" | "enum">;
82
+ /** Minimum number of items in the array. */
83
+ minItems?: number;
84
+ /** Maximum number of items in the array. */
85
+ maxItems?: number;
86
+ /** If `true`, all array items must be unique. */
87
+ uniqueItems?: boolean;
88
+ /** If `true`, marks the field as deprecated in the OpenAPI spec. */
89
+ deprecated?: boolean;
90
+ /** If `true`, the field is only returned in responses (excluded from request bodies). */
91
+ readOnly?: boolean;
92
+ /** If `true`, the field is only accepted in request bodies (excluded from responses). */
93
+ writeOnly?: boolean;
28
94
  };
29
95
  /**
30
96
  * Field-level schema declarations for one or more collections, parsed from `_schema` in the YAML.