@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/index.mjs CHANGED
@@ -151,18 +151,48 @@ function normaliseFieldDef(value) {
151
151
  const v = value;
152
152
  const def = {};
153
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"]))
154
+ if (typeof v["_type"] === "string" && VALID_TYPES.has(v["_type"]))
155
155
  def.type = v["_type"];
156
156
  if (typeof v["_format"] === "string") def.format = v["_format"];
157
157
  if (Array.isArray(v["_enum"])) def.enum = v["_enum"];
158
158
  if (typeof v["_description"] === "string") def.description = v["_description"];
159
159
  if (v["_default"] !== void 0) def.default = v["_default"];
160
+ if (v["_example"] !== void 0) def.example = v["_example"];
161
+ if (v["_nullable"] === true || v["_nullable"] === false) def.nullable = v["_nullable"];
162
+ if (typeof v["_minLength"] === "number") def.minLength = v["_minLength"];
163
+ if (typeof v["_maxLength"] === "number") def.maxLength = v["_maxLength"];
164
+ if (typeof v["_pattern"] === "string") def.pattern = v["_pattern"];
165
+ if (typeof v["_minimum"] === "number") def.minimum = v["_minimum"];
166
+ if (typeof v["_maximum"] === "number") def.maximum = v["_maximum"];
167
+ if (typeof v["_exclusiveMinimum"] === "number") def.exclusiveMinimum = v["_exclusiveMinimum"];
168
+ if (typeof v["_exclusiveMaximum"] === "number") def.exclusiveMaximum = v["_exclusiveMaximum"];
169
+ if (typeof v["_multipleOf"] === "number") def.multipleOf = v["_multipleOf"];
170
+ if (typeof v["_minItems"] === "number") def.minItems = v["_minItems"];
171
+ if (typeof v["_maxItems"] === "number") def.maxItems = v["_maxItems"];
172
+ if (v["_uniqueItems"] === true || v["_uniqueItems"] === false)
173
+ def.uniqueItems = v["_uniqueItems"];
174
+ if (v["_items"] && typeof v["_items"] === "object" && !Array.isArray(v["_items"])) {
175
+ const items = v["_items"];
176
+ const itemsDef = {};
177
+ if (typeof items["_type"] === "string" && VALID_TYPES.has(items["_type"]))
178
+ itemsDef.type = items["_type"];
179
+ if (typeof items["_format"] === "string") itemsDef.format = items["_format"];
180
+ if (Array.isArray(items["_enum"])) itemsDef.enum = items["_enum"];
181
+ def.items = itemsDef;
182
+ }
183
+ if (v["_deprecated"] === true || v["_deprecated"] === false)
184
+ def.deprecated = v["_deprecated"];
185
+ if (v["_readOnly"] === true || v["_readOnly"] === false) def.readOnly = v["_readOnly"];
186
+ if (v["_writeOnly"] === true || v["_writeOnly"] === false)
187
+ def.writeOnly = v["_writeOnly"];
160
188
  return def;
161
189
  }
190
+ var VALID_TYPES;
162
191
  var init_parseSchema = __esm({
163
192
  "src/storage/parseSchema.ts"() {
164
193
  "use strict";
165
194
  init_esm_shims();
195
+ VALID_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean", "object", "array"]);
166
196
  }
167
197
  });
168
198
 
@@ -233,6 +263,30 @@ var init_parseRoutes = __esm({
233
263
  }
234
264
  });
235
265
 
266
+ // src/storage/parseData.ts
267
+ function parseData(raw) {
268
+ const block = raw["_data"];
269
+ const fromBlock = block && typeof block === "object" && !Array.isArray(block) ? Object.fromEntries(
270
+ Object.entries(block).filter(([, v]) => Array.isArray(v))
271
+ ) : {};
272
+ const fromRoot = Object.fromEntries(
273
+ Object.entries(raw).filter(([key, val]) => !RESERVED.has(key) && Array.isArray(val))
274
+ );
275
+ return { ...fromBlock, ...fromRoot };
276
+ }
277
+ function hasDataBlock(raw) {
278
+ const d = raw["_data"];
279
+ return d !== null && d !== void 0 && typeof d === "object" && !Array.isArray(d);
280
+ }
281
+ var RESERVED;
282
+ var init_parseData = __esm({
283
+ "src/storage/parseData.ts"() {
284
+ "use strict";
285
+ init_esm_shims();
286
+ RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema", "_data"]);
287
+ }
288
+ });
289
+
236
290
  // src/storage/yrestStorage.ts
237
291
  var yrestStorage_exports = {};
238
292
  __export(yrestStorage_exports, {
@@ -299,13 +353,11 @@ function serializeScenario(s) {
299
353
  function createYrestStorage(filePath) {
300
354
  const absPath = resolve(filePath);
301
355
  const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
302
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
303
356
  const relations = parseRelations(raw["_rel"]);
304
357
  const routes = parseRoutes(raw["_routes"]);
305
358
  const schema = parseSchema(raw["_schema"]);
306
- const data = Object.fromEntries(
307
- Object.entries(raw).filter(([key]) => !RESERVED.has(key))
308
- );
359
+ const dataBlock = hasDataBlock(raw);
360
+ const data = parseData(raw);
309
361
  let snapshot = {
310
362
  data: deepCopyData(data),
311
363
  relations: { ...relations },
@@ -334,7 +386,11 @@ function createYrestStorage(filePath) {
334
386
  const payload = {};
335
387
  if (Object.keys(relations).length > 0) payload._rel = serializeRelations(relations);
336
388
  if (routes.length > 0) payload._routes = serializeRoutes(routes);
337
- Object.assign(payload, data);
389
+ if (dataBlock) {
390
+ payload._data = data;
391
+ } else {
392
+ Object.assign(payload, data);
393
+ }
338
394
  const tmp = resolve(dirname2(absPath), `.yrest-${randomUUID2()}.tmp`);
339
395
  writeFileSync(tmp, stringify2(payload), "utf8");
340
396
  renameSync(tmp, absPath);
@@ -342,9 +398,7 @@ function createYrestStorage(filePath) {
342
398
  reload() {
343
399
  const fresh = parse2(readFileSync2(absPath, "utf8")) ?? {};
344
400
  const freshRelations = parseRelations(fresh["_rel"]);
345
- const freshData = Object.fromEntries(
346
- Object.entries(fresh).filter(([key]) => !RESERVED.has(key))
347
- );
401
+ const freshData = parseData(fresh);
348
402
  for (const key of Object.keys(data)) delete data[key];
349
403
  Object.assign(data, freshData);
350
404
  for (const key of Object.keys(relations)) delete relations[key];
@@ -377,6 +431,7 @@ var init_yrestStorage = __esm({
377
431
  init_parseRelations();
378
432
  init_parseRoutes();
379
433
  init_parseSchema();
434
+ init_parseData();
380
435
  init_deepCopy();
381
436
  }
382
437
  });
@@ -833,9 +888,15 @@ function generateERData(storage) {
833
888
  const relations = storage.getRelations();
834
889
  const schema = storage.getSchema();
835
890
  const pivotTables = /* @__PURE__ */ new Set();
891
+ const pivotFKs = /* @__PURE__ */ new Map();
836
892
  for (const fields of Object.values(relations)) {
837
893
  for (const def of Object.values(fields)) {
838
- if (def.type === "many2many") pivotTables.add(def.through);
894
+ if (def.type === "many2many") {
895
+ pivotTables.add(def.through);
896
+ if (!pivotFKs.has(def.through)) pivotFKs.set(def.through, /* @__PURE__ */ new Set());
897
+ pivotFKs.get(def.through).add(def.foreignKey);
898
+ pivotFKs.get(def.through).add(def.otherKey);
899
+ }
839
900
  }
840
901
  }
841
902
  const fkFields = /* @__PURE__ */ new Map();
@@ -871,6 +932,9 @@ function generateERData(storage) {
871
932
  for (const [source, fields] of Object.entries(relations)) {
872
933
  for (const [key, def] of Object.entries(fields)) {
873
934
  if (def.type === "many2many") {
935
+ const throughRels = relations[def.through] ?? {};
936
+ const fkRel = throughRels[def.foreignKey];
937
+ const otherRel = throughRels[def.otherKey];
874
938
  erRelations.push({
875
939
  source,
876
940
  target: def.target,
@@ -878,9 +942,14 @@ function generateERData(storage) {
878
942
  label: def.through,
879
943
  through: def.through,
880
944
  foreignKey: def.foreignKey,
881
- otherKey: def.otherKey
945
+ otherKey: def.otherKey,
946
+ fkCarDirect: fkRel?.carDirect,
947
+ fkCarInverse: fkRel?.carInverse,
948
+ otherCarDirect: otherRel?.carDirect,
949
+ otherCarInverse: otherRel?.carInverse
882
950
  });
883
951
  } else {
952
+ if (pivotFKs.get(source)?.has(key)) continue;
884
953
  erRelations.push({
885
954
  source,
886
955
  target: def.target,
@@ -907,7 +976,15 @@ function erDiagramSection(data) {
907
976
 
908
977
  <div class="card" style="padding:0;overflow:hidden;position:relative;">
909
978
  <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>
979
+ <div style="position:absolute;top:10px;right:14px;display:flex;gap:6px;align-items:center;">
980
+ <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;">
981
+ <option value="1">1\xD7</option>
982
+ <option value="2">2\xD7</option>
983
+ <option value="3" selected>3\xD7</option>
984
+ <option value="4">4\xD7</option>
985
+ </select>
986
+ <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>
987
+ </div>
911
988
  </div>
912
989
 
913
990
  <script>${/*js*/
@@ -917,7 +994,7 @@ function erDiagramSection(data) {
917
994
  var D=${json};
918
995
 
919
996
  // \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;
997
+ var NW=178,HH=34,FH=22,FP=10,ROW_GAP=44,COL_GAP=100,BR=12,BO=16,LGH=84,MIN_GAP=10;
921
998
 
922
999
  var C={bg:'#0d1117',nb:'#161b22',nh:'#1c2128',pb:'#0d1117',ph:'#161b22',
923
1000
  bo:'#30363d',bs:'#58a6ff',tx:'#e6edf3',mu:'#7d8590',
@@ -957,6 +1034,30 @@ function erDiagramSection(data) {
957
1034
  }
958
1035
  });
959
1036
 
1037
+ // \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
1038
+ function computeNodeWidths(){
1039
+ nodes.forEach(function(n){
1040
+ var e=n.e;
1041
+ ctx.font='600 12px -apple-system,BlinkMacSystemFont,sans-serif';
1042
+ var hdrW=FP+ctx.measureText(e.name).width+FP;
1043
+ if(e.isPivot){
1044
+ ctx.font='9px monospace';
1045
+ hdrW+=ctx.measureText('pivot').width+FP;
1046
+ }
1047
+ var maxRow=hdrW;
1048
+ e.fields.forEach(function(f){
1049
+ var leftPad=FP+(f.pk||f.fk?16:0);
1050
+ var rightPad=(f.required&&!f.pk?14:0)+FP;
1051
+ ctx.font=(f.required&&!f.pk?'600 ':'')+'11px monospace';
1052
+ var nw=ctx.measureText(f.name).width;
1053
+ ctx.font='10px monospace';
1054
+ var tw=ctx.measureText(f.type).width;
1055
+ maxRow=Math.max(maxRow,leftPad+nw+MIN_GAP+tw+rightPad);
1056
+ });
1057
+ n.w=Math.max(NW,Math.ceil(maxRow));
1058
+ });
1059
+ }
1060
+
960
1061
  // \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
1062
  var groups=[];
962
1063
 
@@ -995,7 +1096,8 @@ function erDiagramSection(data) {
995
1096
  median(adj[b].map(function(x){return np[x]!=null?np[x]:0;}));
996
1097
  });
997
1098
  }
998
- var colW=NW+COL_GAP;
1099
+ var maxNW=nodes.reduce(function(m,n){return Math.max(m,n.w);},NW);
1100
+ var colW=maxNW+COL_GAP;
999
1101
  var totalW=(maxL+1)*colW-COL_GAP;
1000
1102
  var startX=Math.max(20,(W-totalW)/2);
1001
1103
  for(var l=0;l<=maxL;l++){
@@ -1043,37 +1145,37 @@ function erDiagramSection(data) {
1043
1145
 
1044
1146
  var badges=[];
1045
1147
 
1046
- function pushBadge(x,y,ux,uy,many,col){
1047
- badges.push({x:x,y:y,ux:ux,uy:uy,many:many,col:col});
1148
+ // \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
1149
+ // SVG orientation: LEFT side touches the node, RIGHT side connects to the line.
1150
+ // Rotation = Math.atan2(uy,ux) aligns local +x with the outward (away-from-node) direction.
1151
+ var P2D={
1152
+ '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'),
1153
+ '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'),
1154
+ '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'),
1155
+ '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'),
1156
+ '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')
1157
+ };
1158
+
1159
+ function pushBadge(x,y,ux,uy,car,col){
1160
+ badges.push({x:x,y:y,ux:ux,uy:uy,car:car,col:col});
1048
1161
  }
1049
1162
 
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
1163
  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();
1164
+ var p=P2D[b.car]||P2D['n'],sc=BR/40;
1165
+ ctx.save();
1166
+ ctx.translate(b.x+b.ux*BO,b.y+b.uy*BO);
1167
+ ctx.rotate(Math.atan2(b.uy,b.ux));
1168
+ ctx.scale(sc,sc);
1169
+ ctx.translate(-40,-40);
1170
+ ctx.fillStyle=b.col;
1171
+ ctx.fill(p,'evenodd');
1070
1172
  ctx.restore();
1071
1173
  }
1072
1174
 
1073
1175
  function ecol(t){return t==='one2one'?C.o2o:t==='many2many'?C.m2m:C.m2o;}
1074
1176
 
1075
1177
  // offset: pixels to shift perpendicular to the edge direction (for parallel edges)
1076
- function drawEdgeLine(a,b,col,dashed,label,manyA,manyB,off){
1178
+ function drawEdgeLine(a,b,col,dashed,label,carA,carB,off){
1077
1179
  var p1=edgePt(a,b.x+b.w/2,b.y+b.h/2);
1078
1180
  var p2=edgePt(b,a.x+a.w/2,a.y+a.h/2);
1079
1181
  var dx=p2.x-p1.x,dy=p2.y-p1.y,d=Math.sqrt(dx*dx+dy*dy)||1;
@@ -1084,13 +1186,19 @@ function erDiagramSection(data) {
1084
1186
  ctx.setLineDash(dashed?[5,4]:[]);
1085
1187
  ctx.beginPath();ctx.moveTo(q1x,q1y);ctx.lineTo(q2x,q2y);ctx.stroke();
1086
1188
  ctx.setLineDash([]);
1087
- pushBadge(q1x,q1y,ux,uy,manyA,col);
1088
- pushBadge(q2x,q2y,-ux,-uy,manyB,col);
1189
+ pushBadge(q1x,q1y,ux,uy,carA,col);
1190
+ pushBadge(q2x,q2y,-ux,-uy,carB,col);
1089
1191
  if(label){
1090
1192
  var mx=(q1x+q2x)/2,my=(q1y+q2y)/2;
1193
+ var la=Math.atan2(uy,ux);
1194
+ if(la>Math.PI/2||la<-Math.PI/2)la+=Math.PI; // keep text readable
1195
+ var ls=off>0?11:-11; // alternate above/below for parallel edges
1196
+ ctx.save();
1197
+ ctx.translate(mx,my);
1198
+ ctx.rotate(la);
1091
1199
  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';
1200
+ ctx.fillText(label,0,ls);
1201
+ ctx.restore();
1094
1202
  }
1095
1203
  }
1096
1204
 
@@ -1106,14 +1214,14 @@ function erDiagramSection(data) {
1106
1214
  var col=ecol(r.type);
1107
1215
  if(r.type==='many2many'&&r.through&&nIdx[r.through]!=null){
1108
1216
  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});
1217
+ 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});
1218
+ 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});
1111
1219
  }else{
1112
1220
  var sn=nodes[nIdx[r.source]],tn=nodes[nIdx[r.target]];
1113
1221
  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});
1222
+ var carA=r.carInverse||(r.type!=='one2one'?'1..n':'1..1');
1223
+ var carB=r.carDirect||(r.type==='many2many'?'1..n':'1..1');
1224
+ edges.push({a:sn,b:tn,col:col,dashed:false,label:r.label,carA:carA,carB:carB,off:0});
1117
1225
  }
1118
1226
  }
1119
1227
  });
@@ -1172,12 +1280,24 @@ function erDiagramSection(data) {
1172
1280
 
1173
1281
  function drawLegend(){
1174
1282
  var cvW=cv.width/dpr,cvH=cv.height/dpr;
1175
- var y0=cvH-LGH,y1=cvH-LGH+18,y2=cvH-LGH+38;
1283
+ var yL=cvH-LGH,y1=yL+13,y2=yL+35,y3=yL+62;
1176
1284
 
1177
1285
  ctx.fillStyle='#161b22';
1178
- ctx.fillRect(0,y0,cvW,LGH);
1286
+ ctx.fillRect(0,yL,cvW,LGH);
1179
1287
  ctx.strokeStyle='#30363d';ctx.lineWidth=1;
1180
- ctx.beginPath();ctx.moveTo(0,y0);ctx.lineTo(cvW,y0);ctx.stroke();
1288
+ ctx.beginPath();ctx.moveTo(0,yL);ctx.lineTo(cvW,yL);ctx.stroke();
1289
+
1290
+ function legendBadge(cx,cy,car,col,angle){
1291
+ var p=P2D[car]||P2D['n'],sc=8/40;
1292
+ ctx.save();
1293
+ ctx.translate(cx,cy);
1294
+ ctx.rotate(angle);
1295
+ ctx.scale(sc,sc);
1296
+ ctx.translate(-40,-40);
1297
+ ctx.fillStyle=col;
1298
+ ctx.fill(p,'evenodd');
1299
+ ctx.restore();
1300
+ }
1181
1301
 
1182
1302
  // Row 1: relation line types
1183
1303
  var types=[
@@ -1196,42 +1316,43 @@ function erDiagramSection(data) {
1196
1316
  x+=18+ctx.measureText(it.l).width+22;
1197
1317
  });
1198
1318
 
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';
1319
+ // Row 2: relation type examples (pair of badges + line)
1216
1320
  x=14;
1217
1321
  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)'}
1322
+ {carA:'1..n',carB:'1..1',col:C.m2o,l:'many2one (N:1)'},
1323
+ {carA:'1..1',carB:'1..1',col:C.o2o,l:'one2one (1:1)'},
1324
+ {carA:'1..1',carB:'0..n',col:C.m2m,l:'pivot edge (1:0..N)'}
1221
1325
  ];
1222
1326
  pairs.forEach(function(p){
1223
1327
  ctx.strokeStyle=p.col+'55';ctx.lineWidth=1.2;ctx.setLineDash(p.col===C.m2m?[4,3]:[]);
1224
1328
  ctx.beginPath();ctx.moveTo(x+8,y2);ctx.lineTo(x+56,y2);ctx.stroke();
1225
1329
  ctx.setLineDash([]);
1226
- badge2(x+8,y2,p.a,p.col);
1227
- badge2(x+56,y2,p.b,p.col);
1330
+ legendBadge(x+8,y2,p.carA,p.col,0);
1331
+ legendBadge(x+56,y2,p.carB,p.col,Math.PI);
1228
1332
  ctx.fillStyle=C.mu;ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1229
1333
  ctx.fillText(p.l,x+66,y2);
1230
1334
  x+=66+ctx.measureText(p.l).width+24;
1231
1335
  });
1232
1336
 
1337
+ // Row 3: individual symbol meanings
1338
+ var symbols=[
1339
+ {car:'1..1',l:'exactly one'},
1340
+ {car:'0..1',l:'zero or one'},
1341
+ {car:'1..n',l:'one or more'},
1342
+ {car:'0..n',l:'zero or more'},
1343
+ {car:'n', l:'many (pivot)'}
1344
+ ];
1345
+ x=14;
1346
+ ctx.font='10px monospace';ctx.textAlign='left';ctx.textBaseline='middle';
1347
+ symbols.forEach(function(s){
1348
+ legendBadge(x+8,y3,s.car,C.mu,0);
1349
+ ctx.fillStyle=C.mu;
1350
+ ctx.fillText(s.l,x+20,y3);
1351
+ x+=20+ctx.measureText(s.l).width+18;
1352
+ });
1353
+
1233
1354
  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);
1355
+ ctx.fillText('drag \xB7 scroll to zoom \xB7 dbl-click to reset',cvW-14,y1);
1235
1356
  ctx.textBaseline='alphabetic';
1236
1357
  }
1237
1358
 
@@ -1243,7 +1364,7 @@ function erDiagramSection(data) {
1243
1364
  ctx.save();ctx.translate(pan.x,pan.y);ctx.scale(zm,zm);
1244
1365
  badges=[];
1245
1366
  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);});
1367
+ edges.forEach(function(e){drawEdgeLine(e.a,e.b,e.col,e.dashed,e.label,e.carA,e.carB,e.off);});
1247
1368
  nodes.forEach(function(n){drawNode(n);});
1248
1369
  badges.forEach(function(b){drawBadge(b);});
1249
1370
  ctx.restore();
@@ -1289,14 +1410,21 @@ function erDiagramSection(data) {
1289
1410
  });
1290
1411
 
1291
1412
  document.getElementById('er-dl').addEventListener('click',function(){
1292
- var SCALE=3;
1293
- var cw=cv.width/dpr,ch=cv.height/dpr;
1413
+ var SCALE=parseInt(document.getElementById('er-scale').value,10)||3;
1414
+ var pad=36;
1415
+ var bx0=1e9,by0=1e9,bx1=-1e9,by1=-1e9;
1416
+ nodes.forEach(function(n){
1417
+ bx0=Math.min(bx0,n.x);by0=Math.min(by0,n.y);
1418
+ bx1=Math.max(bx1,n.x+n.w);by1=Math.max(by1,n.y+n.h);
1419
+ });
1420
+ var cw=bx1-bx0+pad*2,ch=by1-by0+pad*2+LGH;
1294
1421
  var oc=document.createElement('canvas');
1295
1422
  oc.width=Math.round(cw*SCALE);oc.height=Math.round(ch*SCALE);
1296
- var sav={cv:cv,ctx:ctx,dpr:dpr};
1423
+ var sav={cv:cv,ctx:ctx,dpr:dpr,px:pan.x,py:pan.y,zm:zm};
1297
1424
  cv=oc;ctx=oc.getContext('2d');dpr=SCALE;
1425
+ pan.x=pad-bx0;pan.y=pad-by0;zm=1;
1298
1426
  draw();
1299
- cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;
1427
+ cv=sav.cv;ctx=sav.ctx;dpr=sav.dpr;pan.x=sav.px;pan.y=sav.py;zm=sav.zm;
1300
1428
  var a=document.createElement('a');
1301
1429
  a.download='er-diagram.png';a.href=oc.toDataURL('image/png');a.click();
1302
1430
  });
@@ -1304,6 +1432,7 @@ function erDiagramSection(data) {
1304
1432
  // \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
1433
  requestAnimationFrame(function(){
1306
1434
  resize();
1435
+ computeNodeWidths();
1307
1436
  var r=cv.getBoundingClientRect();
1308
1437
  computeLayout(r.width,r.height);
1309
1438
  var h=requiredH();
@@ -1951,15 +2080,19 @@ function resolveRequestPath(dotPath, req) {
1951
2080
  const [root, ...rest] = dotPath.split(".");
1952
2081
  let value;
1953
2082
  switch (root) {
2083
+ case "_body":
1954
2084
  case "body":
1955
2085
  value = req.body;
1956
2086
  break;
2087
+ case "_params":
1957
2088
  case "params":
1958
2089
  value = req.params;
1959
2090
  break;
2091
+ case "_query":
1960
2092
  case "query":
1961
2093
  value = req.query;
1962
2094
  break;
2095
+ case "_headers":
1963
2096
  case "headers":
1964
2097
  value = req.headers;
1965
2098
  break;
@@ -2260,6 +2393,23 @@ function buildCollectionSchema(items, fieldDefs = {}) {
2260
2393
  if (def?.description) prop.description = def.description;
2261
2394
  if (def?.enum) prop.enum = def.enum;
2262
2395
  if (def?.default !== void 0) prop.default = def.default;
2396
+ if (def?.example !== void 0) prop.example = def.example;
2397
+ if (def?.nullable) prop.nullable = def.nullable;
2398
+ if (def?.minLength !== void 0) prop.minLength = def.minLength;
2399
+ if (def?.maxLength !== void 0) prop.maxLength = def.maxLength;
2400
+ if (def?.pattern) prop.pattern = def.pattern;
2401
+ if (def?.minimum !== void 0) prop.minimum = def.minimum;
2402
+ if (def?.maximum !== void 0) prop.maximum = def.maximum;
2403
+ if (def?.exclusiveMinimum !== void 0) prop.exclusiveMinimum = def.exclusiveMinimum;
2404
+ if (def?.exclusiveMaximum !== void 0) prop.exclusiveMaximum = def.exclusiveMaximum;
2405
+ if (def?.multipleOf !== void 0) prop.multipleOf = def.multipleOf;
2406
+ if (def?.items) prop.items = def.items;
2407
+ if (def?.minItems !== void 0) prop.minItems = def.minItems;
2408
+ if (def?.maxItems !== void 0) prop.maxItems = def.maxItems;
2409
+ if (def?.uniqueItems !== void 0) prop.uniqueItems = def.uniqueItems;
2410
+ if (def?.deprecated) prop.deprecated = def.deprecated;
2411
+ if (def?.readOnly) prop.readOnly = def.readOnly;
2412
+ if (def?.writeOnly) prop.writeOnly = def.writeOnly;
2263
2413
  properties[field] = prop;
2264
2414
  if (def?.required === true) required.push(field);
2265
2415
  }
@@ -2763,12 +2913,12 @@ function buildOptions(opts) {
2763
2913
  }
2764
2914
  function createInMemoryStorage(data) {
2765
2915
  const raw = data;
2766
- const RESERVED = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2916
+ const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2767
2917
  const relations = parseRelations(raw["_rel"]);
2768
2918
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
2769
2919
  const schema = parseSchema(raw["_schema"]);
2770
2920
  const collections = Object.fromEntries(
2771
- Object.entries(raw).filter(([k]) => !RESERVED.has(k))
2921
+ Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
2772
2922
  );
2773
2923
  let snapshot = {
2774
2924
  data: deepCopyData(collections),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.",
5
5
  "keywords": [
6
6
  "yrest",
@@ -60,12 +60,16 @@
60
60
  "dev": "tsup --watch",
61
61
  "test": "vitest",
62
62
  "test:run": "vitest run",
63
+ "test:unit": "vitest run tests/config tests/cli tests/storage tests/utils",
64
+ "test:integration": "vitest run tests/routes tests/openapi tests/security tests/api",
65
+ "test:e2e": "vitest run tests/e2e",
66
+ "test:preflight": "vitest run",
63
67
  "typecheck": "tsc --noEmit",
64
68
  "lint": "eslint src tests",
65
69
  "lint:fix": "eslint src tests --fix",
66
70
  "format": "prettier --write .",
67
71
  "format:check": "prettier --check .",
68
- "prepublishOnly": "npm run test:run && npm run build",
72
+ "prepublishOnly": "npm run build && npm run test:preflight",
69
73
  "version": "node scripts/update-version-badge.mjs && git add README.md"
70
74
  },
71
75
  "dependencies": {