mikro-orm-markdown 0.1.0-alpha.3 → 0.1.0-alpha.5

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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.0-alpha.5] - 2026-06-29
9
+
10
+ ### Added
11
+
12
+ - `--mermaid-layout <layout>` and `--mermaid-theme <theme>` CLI options to inject Mermaid YAML frontmatter into each `erDiagram` fence
13
+ - `mermaid` option in the programmatic API (`generateMarkdown({ mermaid: { layout, theme } })`) with the same effect
14
+ - `MermaidLayout`, `MermaidTheme`, and `MermaidRenderOptions` types exported from the public API
15
+ - When neither option is set, output is identical to previous versions — no frontmatter is emitted
16
+
17
+ ## [0.1.0-alpha.4] - 2026-06-29
18
+
19
+ ### Fixed
20
+
21
+ - `resolveScalarType` now preserves composite primary-key column alignment through FK-as-PK chains
22
+ - `resolveScalarType` now falls back to `integer` after depth 5 instead of leaking an entity class name
23
+ - Cross-namespace ERD filtering now treats `@describe` as a home namespace when checking `@erd` guests
24
+ - Cross-namespace ERD guest entities with no visible primary-key columns are excluded instead of rendering empty boxes
25
+
26
+ ### Changed
27
+
28
+ - Markdown tables and Mermaid ERDs now render database-specific scalar types as generic, database-agnostic types
29
+
8
30
  ## [0.1.0-alpha.3] - 2026-06-29
9
31
 
10
32
  ### Added
package/README.ko.md CHANGED
@@ -79,6 +79,8 @@ npm run erd
79
79
  | `-d, --description <string>` | — | 제목 아래에 표시할 설명 문단 (선택) |
80
80
  | `--tsconfig <path>` | — | `.ts` config 로드 시 사용할 `tsconfig.json`; 기본값은 config 파일 근처의 가장 가까운 파일 |
81
81
  | `--src <paths...>` | — | 원본 TypeScript 엔티티 소스 경로/glob; MikroORM이 컴파일된 JavaScript에서 엔티티를 discovery할 때만 필요 |
82
+ | `--mermaid-layout <layout>` | — | Mermaid 레이아웃 엔진 (`dagre\|elk\|elk.stress`). 생략하면 뷰어 기본값 사용. |
83
+ | `--mermaid-theme <theme>` | — | Mermaid 테마 (`default\|neutral\|dark\|forest\|base`). 생략하면 뷰어 기본값 사용. |
82
84
 
83
85
  > 설명이 길거나 여러 줄이라면 CLI 대신 [프로그래밍 API](#프로그래밍-api)를 사용하세요 — 쉘 인용 부호 제약 없이 문자열을 그대로 전달할 수 있습니다.
84
86
 
@@ -392,6 +394,7 @@ await writeFile('./ERD.md', markdown, 'utf-8');
392
394
  | `description` | 제목 아래에 표시할 설명 문단입니다. CLI flag와 달리 쉘 quoting 제약 없이 문자열을 그대로 전달할 수 있습니다. |
393
395
  | `src` | 원본 TypeScript 엔티티 소스 경로/glob입니다. `orm.entities`가 컴파일된 JavaScript를 discovery할 때만 필요합니다. |
394
396
  | `onWarn` | 컴파일된 JavaScript에서 JSDoc을 읽지 못하는 상황 같은 non-fatal warning을 받을 callback입니다. |
397
+ | `mermaid` | Mermaid 렌더링 옵션입니다. 아래 [Mermaid 렌더링 옵션](#mermaid-렌더링-옵션)을 참고하세요. |
395
398
 
396
399
  MikroORM config를 비동기적으로 만들어야 한다면 직접 resolve한 뒤 결과 options object를 전달하세요:
397
400
 
@@ -400,6 +403,56 @@ const ormConfig = await createOrmConfig();
400
403
  const markdown = await generateMarkdown({ orm: ormConfig });
401
404
  ```
402
405
 
406
+ ### Mermaid 렌더링 옵션
407
+
408
+ 기본적으로 mikro-orm-markdown은 Mermaid frontmatter config를 출력하지 않습니다. 이렇게 해야 각 Markdown 뷰어의 기본 Mermaid 렌더링 동작이 유지됩니다.
409
+
410
+ CLI에서 Mermaid 렌더링 옵션을 사용하려면:
411
+
412
+ ```bash
413
+ mikro-orm-markdown --config ./mikro-orm.config.ts --mermaid-layout elk
414
+ mikro-orm-markdown --config ./mikro-orm.config.ts --mermaid-theme forest
415
+ mikro-orm-markdown --config ./mikro-orm.config.ts --mermaid-layout elk --mermaid-theme neutral
416
+ ```
417
+
418
+ 프로그래밍 API에서는:
419
+
420
+ ```typescript
421
+ const markdown = await generateMarkdown({
422
+ orm: ormConfig,
423
+ mermaid: {
424
+ layout: 'elk',
425
+ theme: 'forest',
426
+ },
427
+ });
428
+ ```
429
+
430
+ layout 또는 theme을 지정하면 각 `erDiagram` 펜스 앞에 YAML frontmatter 블록이 삽입됩니다:
431
+
432
+ ````markdown
433
+ ```mermaid
434
+ ---
435
+ config:
436
+ layout: elk
437
+ theme: forest
438
+ ---
439
+ erDiagram
440
+ ...
441
+ ```
442
+ ````
443
+
444
+ **사용 가능한 layout 값:**
445
+
446
+ | 값 | 설명 |
447
+ | -- | ---- |
448
+ | `dagre` | Mermaid 기본 레이아웃 엔진. |
449
+ | `elk` | 대체 레이아웃 엔진. 엔티티가 많거나 관계선이 복잡한 ERD에서 선 라우팅을 개선할 수 있습니다. |
450
+ | `elk.stress` | ELK stress 레이아웃 변형. Mermaid 버전과 뷰어에 따라 지원 여부가 다릅니다. |
451
+
452
+ **사용 가능한 theme 값:** `default`, `neutral`, `dark`, `forest`, `base`.
453
+
454
+ > `elk`와 theme 지원 여부는 뷰어마다 다릅니다. `--mermaid-layout`이나 `--mermaid-theme`을 지정하지 않으면 frontmatter는 출력되지 않습니다.
455
+
403
456
  ## 라이선스
404
457
 
405
458
  MIT
package/README.md CHANGED
@@ -79,6 +79,8 @@ npm run erd
79
79
  | `-d, --description <string>` | — | Optional description paragraph shown below the title |
80
80
  | `--tsconfig <path>` | — | `tsconfig.json` used when loading a `.ts` config; defaults to the nearest one beside the config file |
81
81
  | `--src <paths...>` | — | Original TypeScript entity source paths/globs; only needed when MikroORM discovers entities from compiled JavaScript |
82
+ | `--mermaid-layout <layout>` | — | Mermaid layout engine (`dagre\|elk\|elk.stress`). Omit to use the viewer's default. |
83
+ | `--mermaid-theme <theme>` | — | Mermaid theme (`default\|neutral\|dark\|forest\|base`). Omit to use the viewer's default. |
82
84
 
83
85
  > For a long or multiline description, use the [programmatic API](#programmatic-api) instead — it accepts any string directly, without shell quoting limits.
84
86
 
@@ -392,6 +394,7 @@ Programmatic options:
392
394
  | `description` | Optional paragraph below the title. Unlike the CLI flag, this can be any string without shell quoting concerns. |
393
395
  | `src` | Original TypeScript entity source paths/globs. Only needed when `orm.entities` discovers compiled JavaScript. |
394
396
  | `onWarn` | Callback for non-fatal warnings, such as compiled JavaScript JSDoc loss. |
397
+ | `mermaid` | Optional Mermaid rendering options. See [Mermaid rendering options](#mermaid-rendering-options) below. |
395
398
 
396
399
  If your MikroORM config is asynchronous, resolve it yourself and pass the resulting options object:
397
400
 
@@ -400,6 +403,56 @@ const ormConfig = await createOrmConfig();
400
403
  const markdown = await generateMarkdown({ orm: ormConfig });
401
404
  ```
402
405
 
406
+ ### Mermaid rendering options
407
+
408
+ By default, mikro-orm-markdown does not emit Mermaid frontmatter config. This preserves each Markdown viewer's default Mermaid rendering behavior.
409
+
410
+ You can opt into Mermaid rendering options via the CLI:
411
+
412
+ ```bash
413
+ mikro-orm-markdown --config ./mikro-orm.config.ts --mermaid-layout elk
414
+ mikro-orm-markdown --config ./mikro-orm.config.ts --mermaid-theme forest
415
+ mikro-orm-markdown --config ./mikro-orm.config.ts --mermaid-layout elk --mermaid-theme neutral
416
+ ```
417
+
418
+ Or via the programmatic API:
419
+
420
+ ```typescript
421
+ const markdown = await generateMarkdown({
422
+ orm: ormConfig,
423
+ mermaid: {
424
+ layout: 'elk',
425
+ theme: 'forest',
426
+ },
427
+ });
428
+ ```
429
+
430
+ When a layout or theme is set, a YAML frontmatter block is prepended to each `erDiagram` fence:
431
+
432
+ ````markdown
433
+ ```mermaid
434
+ ---
435
+ config:
436
+ layout: elk
437
+ theme: forest
438
+ ---
439
+ erDiagram
440
+ ...
441
+ ```
442
+ ````
443
+
444
+ **Available layout values:**
445
+
446
+ | Value | Description |
447
+ | ----- | ----------- |
448
+ | `dagre` | Mermaid's default layered layout. |
449
+ | `elk` | Alternative layout engine; can improve line routing on larger or denser ERDs. |
450
+ | `elk.stress` | ELK stress layout variant. Support varies by Mermaid version and viewer. |
451
+
452
+ **Available theme values:** `default`, `neutral`, `dark`, `forest`, `base`.
453
+
454
+ > Viewer support for `elk` and theme values varies. If no `--mermaid-layout` or `--mermaid-theme` is provided, no frontmatter is emitted.
455
+
403
456
  ## License
404
457
 
405
458
  MIT
package/dist/cli.cjs CHANGED
@@ -281,6 +281,8 @@ function toMarkdownAnchor(value) {
281
281
  }
282
282
 
283
283
  // src/render/mermaid.ts
284
+ var MERMAID_LAYOUTS = ["dagre", "elk", "elk.stress"];
285
+ var MERMAID_THEMES = ["default", "neutral", "dark", "forest", "base"];
284
286
  var FORMULA_DUMMY_TABLE = {
285
287
  alias: "e0",
286
288
  name: "",
@@ -419,9 +421,31 @@ function resolveFkTypes(prop, metaByClass, fieldNameCount) {
419
421
  return Array.from({ length: fieldNameCount }, (_value, index) => {
420
422
  const referencedColumnName = referencedColumnNames[index];
421
423
  const pkProp = referencedColumnName !== void 0 ? primaryProps.find((candidate) => candidate.fieldNames.includes(referencedColumnName)) : void 0;
422
- return (pkProp ?? primaryProps[index] ?? primaryProps[0])?.type ?? "integer";
424
+ const resolvedProp = pkProp ?? primaryProps[index] ?? primaryProps[0];
425
+ const rawType = resolvedProp?.type ?? "integer";
426
+ const pkIndex = resolvedProp !== void 0 ? primaryProps.indexOf(resolvedProp) : 0;
427
+ return resolveScalarType(rawType, metaByClass, pkIndex);
423
428
  });
424
429
  }
430
+ function resolveScalarType(type, metaByClass, pkIndex = 0, depth = 0) {
431
+ if (depth >= 5) {
432
+ return "integer";
433
+ }
434
+ const refMeta = metaByClass.get(type);
435
+ if (!refMeta) {
436
+ return type;
437
+ }
438
+ const primaryProps = getPrimaryProps(refMeta);
439
+ if (primaryProps.length === 0) {
440
+ return type;
441
+ }
442
+ const targetProp = primaryProps[pkIndex] ?? primaryProps[0];
443
+ const nextType = targetProp?.type ?? type;
444
+ if (nextType === type) {
445
+ return type;
446
+ }
447
+ return resolveScalarType(nextType, metaByClass, pkIndex, depth + 1);
448
+ }
425
449
  function getPrimaryProps(meta) {
426
450
  const primaryKeys = meta.primaryKeys ?? [];
427
451
  const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
@@ -523,8 +547,41 @@ function buildEdge(fromEntity, prop) {
523
547
  }
524
548
  return null;
525
549
  }
526
- function renderErDiagram(model) {
527
- const lines = ["erDiagram"];
550
+ function normalizeType(type) {
551
+ const t = type.toLowerCase().trim();
552
+ if (t === "uuid" || t === "text" || t === "string" || t.startsWith("varchar")) {
553
+ return "string";
554
+ }
555
+ if (t === "timestamptz" || t === "timestamp" || t === "datetime") {
556
+ return "datetime";
557
+ }
558
+ if (t === "integer" || t === "int" || t === "bigint" || t === "smallint") {
559
+ return "integer";
560
+ }
561
+ if (t === "doubletype" || t === "double precision" || t === "double" || t === "float" || t === "decimal") {
562
+ return "float";
563
+ }
564
+ if (t === "boolean" || t === "bool") {
565
+ return "boolean";
566
+ }
567
+ if (t === "jsonb") {
568
+ return "json";
569
+ }
570
+ return type;
571
+ }
572
+ function renderErDiagram(model, mermaid) {
573
+ const lines = [];
574
+ if (mermaid?.layout !== void 0 || mermaid?.theme !== void 0) {
575
+ lines.push("---", "config:");
576
+ if (mermaid.layout !== void 0) {
577
+ lines.push(` layout: ${mermaid.layout}`);
578
+ }
579
+ if (mermaid.theme !== void 0) {
580
+ lines.push(` theme: ${mermaid.theme}`);
581
+ }
582
+ lines.push("---");
583
+ }
584
+ lines.push("erDiagram");
528
585
  for (const entity of model.entities) {
529
586
  lines.push(` ${toMermaidIdentifier(entity.className)} {`);
530
587
  for (const col of entity.columns) {
@@ -557,7 +614,7 @@ function renderColumnLine(col) {
557
614
  comment = "self-ref";
558
615
  }
559
616
  const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
560
- return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
617
+ return `${toMermaidIdentifier(normalizeType(col.type))} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
561
618
  }
562
619
 
563
620
  // src/model/build.ts
@@ -604,9 +661,16 @@ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
604
661
  const groups = [];
605
662
  for (const groupName of groupNames) {
606
663
  const isDefault = groupName === "default";
607
- const erdEntities = [...enrichedByClass.values()].filter(
608
- ({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)
609
- );
664
+ const erdEntities = [...enrichedByClass.values()].filter(({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)).map((entity) => {
665
+ if (isCrossNamespaceInGroup(entity.jsDoc, groupName, isDefault)) {
666
+ const pkColumns = entity.model.columns.filter((col) => col.isPrimary);
667
+ if (pkColumns.length === 0) {
668
+ return null;
669
+ }
670
+ return { ...entity, model: { ...entity.model, columns: pkColumns } };
671
+ }
672
+ return entity;
673
+ }).filter((entity) => entity !== null);
610
674
  const textEntities = [...enrichedByClass.values()].filter(
611
675
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
612
676
  );
@@ -708,6 +772,12 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
708
772
  }
709
773
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
710
774
  }
775
+ function isCrossNamespaceInGroup(jsDoc, groupName, isDefault) {
776
+ if (isDefault || !jsDoc) {
777
+ return false;
778
+ }
779
+ return jsDoc.erdNamespaces.includes(groupName) && !jsDoc.namespaces.includes(groupName) && !jsDoc.describeNamespaces.includes(groupName);
780
+ }
711
781
 
712
782
  // src/provider.ts
713
783
  async function withTsMorphMetadataProvider(options, onWarn) {
@@ -730,7 +800,7 @@ async function withTsMorphMetadataProvider(options, onWarn) {
730
800
  }
731
801
 
732
802
  // src/render/markdown.ts
733
- function renderMarkdown(docModel) {
803
+ function renderMarkdown(docModel, mermaid) {
734
804
  const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
735
805
  if (docModel.description) {
736
806
  sections.push(escapeMarkdownParagraph(docModel.description));
@@ -739,7 +809,7 @@ function renderMarkdown(docModel) {
739
809
  sections.push(renderTableOfContents(docModel.groups));
740
810
  }
741
811
  for (const group of docModel.groups) {
742
- sections.push(renderGroupSection(group));
812
+ sections.push(renderGroupSection(group, mermaid));
743
813
  }
744
814
  return sections.join("\n\n");
745
815
  }
@@ -756,14 +826,14 @@ function renderTableOfContents(groups) {
756
826
  return `- [${label}](#${toMarkdownAnchor(group.name)})`;
757
827
  });
758
828
  }
759
- function renderGroupSection(group) {
829
+ function renderGroupSection(group, mermaid) {
760
830
  const parts = [`## ${escapeMarkdownInline(group.name)}`];
761
831
  if (group.erdEntities.length > 0) {
762
832
  const diagramModel = {
763
833
  entities: group.erdEntities.map((e) => e.model),
764
834
  relations: group.erdRelations
765
835
  };
766
- parts.push("```mermaid\n" + renderErDiagram(diagramModel) + "\n```");
836
+ parts.push("```mermaid\n" + renderErDiagram(diagramModel, mermaid) + "\n```");
767
837
  }
768
838
  for (const entity of group.textEntities) {
769
839
  parts.push(renderEntitySection(entity));
@@ -805,7 +875,7 @@ function renderColumnTable(entity) {
805
875
  const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
806
876
  const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
807
877
  const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
808
- return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(col.type)} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
878
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(normalizeType(col.type))} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
809
879
  });
810
880
  return [header, sep, ...rows].join("\n");
811
881
  }
@@ -919,7 +989,7 @@ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm)
919
989
  }
920
990
  }
921
991
  async function generateMarkdown(options) {
922
- const { orm, title = "Database Schema", description, src, onWarn } = options;
992
+ const { orm, title = "Database Schema", description, src, onWarn, mermaid } = options;
923
993
  const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
924
994
  const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
925
995
  const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
@@ -927,7 +997,7 @@ async function generateMarkdown(options) {
927
997
  assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
928
998
  }
929
999
  const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
930
- return renderMarkdown(docModel);
1000
+ return renderMarkdown(docModel, mermaid);
931
1001
  }
932
1002
 
933
1003
  // src/cli.ts
@@ -1042,9 +1112,34 @@ async function writeMarkdownFile(outPath, markdown) {
1042
1112
  ${formatFileSystemError(cause)}`, { cause });
1043
1113
  }
1044
1114
  }
1115
+ function parseMermaidOptions(opts) {
1116
+ const { mermaidLayout, mermaidTheme } = opts;
1117
+ if (mermaidLayout !== void 0 && !MERMAID_LAYOUTS.includes(mermaidLayout)) {
1118
+ process.stderr.write(
1119
+ `Error: Invalid --mermaid-layout "${mermaidLayout}". Allowed values: ${MERMAID_LAYOUTS.join(", ")}
1120
+ `
1121
+ );
1122
+ process.exit(1);
1123
+ }
1124
+ if (mermaidTheme !== void 0 && !MERMAID_THEMES.includes(mermaidTheme)) {
1125
+ process.stderr.write(
1126
+ `Error: Invalid --mermaid-theme "${mermaidTheme}". Allowed values: ${MERMAID_THEMES.join(", ")}
1127
+ `
1128
+ );
1129
+ process.exit(1);
1130
+ }
1131
+ if (mermaidLayout === void 0 && mermaidTheme === void 0) {
1132
+ return void 0;
1133
+ }
1134
+ return {
1135
+ ...mermaidLayout !== void 0 && { layout: mermaidLayout },
1136
+ ...mermaidTheme !== void 0 && { theme: mermaidTheme }
1137
+ };
1138
+ }
1045
1139
  async function run(opts) {
1046
1140
  const configPath = path2.resolve(opts.config);
1047
1141
  const outPath = path2.resolve(opts.out);
1142
+ const mermaid = parseMermaidOptions(opts);
1048
1143
  let ormOptions;
1049
1144
  try {
1050
1145
  ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
@@ -1063,6 +1158,7 @@ ${err instanceof Error ? err.message : String(err)}
1063
1158
  title: opts.title,
1064
1159
  ...opts.description !== void 0 && { description: opts.description },
1065
1160
  ...opts.src !== void 0 && { src: opts.src },
1161
+ ...mermaid !== void 0 && { mermaid },
1066
1162
  onWarn: (message) => void process.stderr.write(`Warning: ${message}
1067
1163
  `)
1068
1164
  });
@@ -1087,7 +1183,7 @@ var program = new import_commander.Command().name("mikro-orm-markdown").descript
1087
1183
  ).option(
1088
1184
  "--src <paths...>",
1089
1185
  "Source .ts file globs to read JSDoc from when entities run from compiled .js (comments are stripped at build time)"
1090
- ).action(run);
1186
+ ).option("--mermaid-layout <layout>", `Mermaid layout engine injected as frontmatter (${MERMAID_LAYOUTS.join("|")})`).option("--mermaid-theme <theme>", `Mermaid theme injected as frontmatter (${MERMAID_THEMES.join("|")})`).action(run);
1091
1187
  function isDirectCliExecution() {
1092
1188
  const entryPoint = process.argv[1];
1093
1189
  if (entryPoint === void 0) {