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

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.js CHANGED
@@ -1,9 +1,12 @@
1
1
  // src/docs/jsdoc.ts
2
2
  import { Project } from "ts-morph";
3
- function loadJsDoc(srcGlobs) {
3
+ function loadJsDoc(filePaths) {
4
4
  const entities = /* @__PURE__ */ new Map();
5
5
  const props = /* @__PURE__ */ new Map();
6
- if (srcGlobs.length === 0) return { entities, props };
6
+ const classNames = /* @__PURE__ */ new Set();
7
+ if (filePaths.length === 0) {
8
+ return { entities, props, sourceFileCount: 0, classNames };
9
+ }
7
10
  const project = new Project({
8
11
  skipAddingFilesFromTsConfig: true,
9
12
  skipLoadingLibFiles: true,
@@ -12,30 +15,44 @@ function loadJsDoc(srcGlobs) {
12
15
  skipLibCheck: true
13
16
  }
14
17
  });
15
- project.addSourceFilesAtPaths(srcGlobs);
16
- for (const sourceFile of project.getSourceFiles()) {
17
- for (const cls of sourceFile.getClasses()) {
18
- const className = cls.getName();
19
- if (!className) continue;
20
- const classDocs = cls.getJsDocs();
21
- if (classDocs.length > 0) {
22
- entities.set(className, parseEntityJsDoc(classDocs));
23
- }
24
- const propMap = /* @__PURE__ */ new Map();
25
- for (const prop of cls.getProperties()) {
26
- const propDocs = prop.getJsDocs();
27
- if (propDocs.length === 0) continue;
28
- const desc = extractDescription(propDocs);
29
- if (desc !== void 0) {
30
- propMap.set(prop.getName(), { description: desc });
18
+ for (const filePath of filePaths) {
19
+ try {
20
+ project.addSourceFilesAtPaths(filePath);
21
+ } catch {
22
+ }
23
+ }
24
+ const sourceFiles = project.getSourceFiles();
25
+ for (const sourceFile of sourceFiles) {
26
+ try {
27
+ for (const cls of sourceFile.getClasses()) {
28
+ const className = cls.getName();
29
+ if (!className) {
30
+ continue;
31
+ }
32
+ classNames.add(className);
33
+ const classDocs = cls.getJsDocs();
34
+ if (classDocs.length > 0) {
35
+ entities.set(className, parseEntityJsDoc(classDocs));
36
+ }
37
+ const propMap = /* @__PURE__ */ new Map();
38
+ for (const prop of cls.getProperties()) {
39
+ const propDocs = prop.getJsDocs();
40
+ if (propDocs.length === 0) {
41
+ continue;
42
+ }
43
+ const info = parsePropJsDoc(propDocs);
44
+ if (info.description !== void 0 || info.atLeastOne) {
45
+ propMap.set(prop.getName(), info);
46
+ }
47
+ }
48
+ if (propMap.size > 0) {
49
+ props.set(className, propMap);
31
50
  }
32
51
  }
33
- if (propMap.size > 0) {
34
- props.set(className, propMap);
35
- }
52
+ } catch {
36
53
  }
37
54
  }
38
- return { entities, props };
55
+ return { entities, props, sourceFileCount: sourceFiles.length, classNames };
39
56
  }
40
57
  function parseEntityJsDoc(jsDocs) {
41
58
  const namespaces = [];
@@ -45,14 +62,21 @@ function parseEntityJsDoc(jsDocs) {
45
62
  let description;
46
63
  for (const doc of jsDocs) {
47
64
  const desc = doc.getDescription().trim();
48
- if (desc && description === void 0) description = desc;
65
+ if (desc && description === void 0) {
66
+ description = desc;
67
+ }
49
68
  for (const tag of doc.getTags()) {
50
69
  const tagName = tag.getTagName();
51
70
  const comment = tag.getCommentText()?.trim();
52
- if (tagName === "namespace" && comment) namespaces.push(comment);
53
- else if (tagName === "erd" && comment) erdNamespaces.push(comment);
54
- else if (tagName === "describe" && comment) describeNamespaces.push(comment);
55
- else if (tagName === "hidden") hidden = true;
71
+ if (tagName === "namespace" && comment) {
72
+ namespaces.push(comment);
73
+ } else if (tagName === "erd" && comment) {
74
+ erdNamespaces.push(comment);
75
+ } else if (tagName === "describe" && comment) {
76
+ describeNamespaces.push(comment);
77
+ } else if (tagName === "hidden") {
78
+ hidden = true;
79
+ }
56
80
  }
57
81
  }
58
82
  return {
@@ -63,16 +87,26 @@ function parseEntityJsDoc(jsDocs) {
63
87
  hidden
64
88
  };
65
89
  }
66
- function extractDescription(jsDocs) {
90
+ function parsePropJsDoc(jsDocs) {
91
+ let description;
92
+ let atLeastOne = false;
67
93
  for (const doc of jsDocs) {
68
94
  const desc = doc.getDescription().trim();
69
- if (desc) return desc;
95
+ if (desc && description === void 0) {
96
+ description = desc;
97
+ }
98
+ for (const tag of doc.getTags()) {
99
+ if (tag.getTagName() === "atLeastOne") {
100
+ atLeastOne = true;
101
+ }
102
+ }
70
103
  }
71
- return void 0;
104
+ return { ...description !== void 0 && { description }, atLeastOne };
72
105
  }
73
106
 
74
107
  // src/metadata/load.ts
75
- import { MikroORM } from "@mikro-orm/core";
108
+ import * as path from "path";
109
+ import { EntitySchema, MikroORM } from "@mikro-orm/core";
76
110
  var MetadataLoadError = class extends Error {
77
111
  constructor(message, cause) {
78
112
  super(message);
@@ -80,12 +114,46 @@ var MetadataLoadError = class extends Error {
80
114
  this.name = "MetadataLoadError";
81
115
  }
82
116
  };
117
+ async function closeDiscoveryResources(orm) {
118
+ await orm.config.getMetadataCacheAdapter()?.close?.();
119
+ await orm.config.getResultCacheAdapter()?.close?.();
120
+ }
121
+ function collectEntitySchemaNames(options) {
122
+ const configuredEntities = [...options.entities ?? [], ...options.entitiesTs ?? []];
123
+ const names = [];
124
+ for (const entity of configuredEntities) {
125
+ if (entity instanceof EntitySchema) {
126
+ names.push(entity.meta.className);
127
+ continue;
128
+ }
129
+ if (entity !== null && typeof entity === "object" && "schema" in entity && entity.schema instanceof EntitySchema) {
130
+ names.push(entity.schema.meta.className);
131
+ }
132
+ }
133
+ return names;
134
+ }
135
+ function assertNoEntitySchemaEntities(options) {
136
+ const schemaNames = collectEntitySchemaNames(options);
137
+ if (schemaNames.length === 0) {
138
+ return;
139
+ }
140
+ throw new MetadataLoadError(
141
+ `EntitySchema-defined entities are not currently supported: ${schemaNames.join(", ")}.
142
+ Use decorator-based @Entity() classes instead.`
143
+ );
144
+ }
83
145
  async function loadEntityMetadata(options) {
146
+ assertNoEntitySchemaEntities(options);
84
147
  let orm;
85
148
  try {
86
149
  orm = await MikroORM.init({
87
150
  ...options,
88
- debug: false
151
+ debug: false,
152
+ connect: false,
153
+ // Always disable the metadata cache for one-shot doc runs so the project
154
+ // is never littered with a temp/ folder, regardless of how metadataProvider
155
+ // was configured.
156
+ metadataCache: { ...options.metadataCache, enabled: false }
89
157
  });
90
158
  } catch (cause) {
91
159
  throw new MetadataLoadError(
@@ -100,20 +168,73 @@ async function loadEntityMetadata(options) {
100
168
  "No entities were discovered. Check that your config specifies at least one entity path or class."
101
169
  );
102
170
  }
103
- return all;
171
+ const baseDir = orm.config.get("baseDir");
172
+ const sourcePaths = [...new Set(all.filter((m) => m.path).map((m) => path.resolve(baseDir, m.path)))];
173
+ return { metas: all, sourcePaths };
104
174
  } finally {
105
- await orm.close(true);
175
+ await closeDiscoveryResources(orm);
106
176
  }
107
177
  }
108
178
 
179
+ // src/model/build.ts
180
+ import { ReferenceKind as ReferenceKind2 } from "@mikro-orm/core";
181
+
109
182
  // src/render/mermaid.ts
110
183
  import { ReferenceKind } from "@mikro-orm/core";
184
+
185
+ // src/render/escape.ts
186
+ var MARKDOWN_INLINE_SPECIAL_CHARS = /[\\`|*#]/g;
187
+ var MERMAID_IDENTIFIER_INVALID_CHARS = /[^a-zA-Z0-9_]/g;
188
+ function normalizeInlineText(value) {
189
+ return value.replace(/\r\n?/g, "\n").replace(/[\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
190
+ }
191
+ function splitNormalizedLines(value) {
192
+ return value.replace(/\r\n?/g, "\n").split("\n");
193
+ }
194
+ function escapeHtmlText(value) {
195
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
196
+ }
197
+ function escapeMarkdownInline(value) {
198
+ return escapeHtmlText(normalizeInlineText(value)).replace(MARKDOWN_INLINE_SPECIAL_CHARS, "\\$&");
199
+ }
200
+ function escapeMarkdownTableCell(value) {
201
+ return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join("<br>");
202
+ }
203
+ function escapeMarkdownParagraph(value) {
204
+ return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join(" \n");
205
+ }
206
+ function renderMarkdownBlockQuote(value) {
207
+ return splitNormalizedLines(value).map((line) => `> ${escapeMarkdownInline(line)}`).join("\n");
208
+ }
209
+ function renderMarkdownInlineCode(value) {
210
+ const normalized = normalizeInlineText(value);
211
+ const backtickRuns = normalized.match(/`+/g) ?? [];
212
+ const longestRun = Math.max(0, ...backtickRuns.map((run) => run.length));
213
+ const fence = "`".repeat(longestRun + 1);
214
+ const needsPadding = normalized.startsWith("`") || normalized.endsWith("`");
215
+ const content = needsPadding ? ` ${normalized} ` : normalized;
216
+ return `${fence}${content}${fence}`;
217
+ }
218
+ function toMermaidIdentifier(value) {
219
+ const normalized = normalizeInlineText(value).replace(MERMAID_IDENTIFIER_INVALID_CHARS, "_").replace(/_+/g, "_");
220
+ const identifier = normalized === "" ? "_" : normalized;
221
+ return /^[a-zA-Z_]/.test(identifier) ? identifier : `_${identifier}`;
222
+ }
223
+ function escapeMermaidQuotedText(value) {
224
+ return normalizeInlineText(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
225
+ }
226
+ function toMarkdownAnchor(value) {
227
+ return normalizeInlineText(value).toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-");
228
+ }
229
+
230
+ // src/render/mermaid.ts
111
231
  var FORMULA_DUMMY_TABLE = {
112
232
  alias: "e0",
113
233
  name: "",
114
234
  qualifiedName: "",
115
235
  toString: () => "e0"
116
236
  };
237
+ var UNRESOLVED_FORMULA = "<unresolved>";
117
238
  function buildDiagramModel(metas) {
118
239
  const metaByClass = new Map(metas.map((m) => [m.className, m]));
119
240
  const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
@@ -121,13 +242,14 @@ function buildDiagramModel(metas) {
121
242
  return { entities, relations };
122
243
  }
123
244
  function buildEntityModel(meta, metaByClass) {
124
- const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.discriminatorValue;
245
+ const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
125
246
  const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
126
247
  const columns = [];
127
248
  for (const prop of Object.values(meta.properties)) {
128
- if (isStiRoot && prop.inherited === true) continue;
129
- const col = buildColumn(prop, metaByClass, meta);
130
- if (col !== null) columns.push(col);
249
+ if (isStiRoot && prop.inherited === true) {
250
+ continue;
251
+ }
252
+ columns.push(...buildColumns(prop, metaByClass, meta));
131
253
  }
132
254
  return {
133
255
  className: meta.className,
@@ -137,61 +259,123 @@ function buildEntityModel(meta, metaByClass) {
137
259
  isEmbeddable: meta.embeddable === true,
138
260
  ...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
139
261
  ...isStiChild && { extendsEntity: meta.extends },
262
+ ...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
140
263
  constraints: buildConstraints(meta)
141
264
  };
142
265
  }
143
- function buildColumn(prop, metaByClass, owningMeta) {
144
- if (prop.kind === ReferenceKind.EMBEDDED) return null;
266
+ function buildColumns(prop, metaByClass, owningMeta) {
267
+ if (prop.kind === ReferenceKind.EMBEDDED) {
268
+ if (prop.object === true || prop.array === true) {
269
+ return [
270
+ {
271
+ propName: prop.name,
272
+ fieldName: prop.fieldNames?.[0] ?? prop.name,
273
+ type: "json",
274
+ isPrimary: false,
275
+ isForeignKey: false,
276
+ isUnique: prop.unique === true,
277
+ isNullable: prop.nullable === true,
278
+ ...prop.comment !== void 0 && { comment: prop.comment },
279
+ embeddedIn: prop.array === true ? `${prop.type}[]` : prop.type
280
+ }
281
+ ];
282
+ }
283
+ return [];
284
+ }
145
285
  if (prop.kind === ReferenceKind.SCALAR) {
286
+ if (prop.object === true && prop.embedded !== void 0) {
287
+ return [];
288
+ }
146
289
  const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
147
290
  let embeddedIn;
291
+ let embeddedPropName;
148
292
  if (prop.embedded !== void 0) {
149
293
  const parentPropName = prop.embedded[0];
150
294
  embeddedIn = owningMeta.properties[parentPropName]?.type;
295
+ embeddedPropName = prop.embedded[1];
151
296
  }
152
297
  const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
153
- return {
154
- propName: prop.name,
155
- fieldName: prop.fieldNames?.[0] ?? prop.name,
156
- type: normalizeType(prop.type),
157
- isPrimary: prop.primary === true,
158
- isForeignKey: false,
159
- isUnique: prop.unique === true,
160
- isNullable: prop.nullable === true,
161
- ...formulaExpr !== void 0 && { formula: formulaExpr },
162
- ...embeddedIn !== void 0 && { embeddedIn },
163
- ...isDiscriminator && { isDiscriminator: true }
164
- };
298
+ const enumItems = prop.enum === true && Array.isArray(prop.items) && prop.items.length > 0 ? prop.items.map((item) => String(item)) : void 0;
299
+ return [
300
+ {
301
+ propName: prop.name,
302
+ fieldName: prop.fieldNames?.[0] ?? prop.name,
303
+ // Store the original type (e.g. `varchar(255)`); the Mermaid renderer
304
+ // sanitizes it for diagram identifiers, while the markdown table shows
305
+ // it verbatim. Guard against a missing type so downstream string
306
+ // handling never sees undefined (matches the FK path's defaulting).
307
+ type: prop.type ?? "unknown",
308
+ isPrimary: prop.primary === true,
309
+ isForeignKey: false,
310
+ isUnique: prop.unique === true,
311
+ isNullable: prop.nullable === true,
312
+ ...prop.comment !== void 0 && { comment: prop.comment },
313
+ ...formulaExpr !== void 0 && { formula: formulaExpr },
314
+ ...embeddedIn !== void 0 && { embeddedIn },
315
+ ...embeddedPropName !== void 0 && { embeddedPropName },
316
+ ...isDiscriminator && { isDiscriminator: true },
317
+ ...enumItems !== void 0 && { enumItems }
318
+ }
319
+ ];
165
320
  }
166
321
  if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner === true) {
167
- const fkType = resolveFkType(prop.type, metaByClass);
168
- return {
169
- propName: prop.name,
170
- fieldName: prop.fieldNames?.[0] ?? `${prop.name}_id`,
171
- type: fkType,
172
- isPrimary: false,
173
- isForeignKey: true,
174
- isUnique: prop.unique === true,
175
- isNullable: prop.nullable === true
176
- };
322
+ const cols = buildForeignKeyColumns(prop, metaByClass);
323
+ if (prop.type === owningMeta.className) {
324
+ return cols.map((col) => ({ ...col, isSelfReference: true }));
325
+ }
326
+ return cols;
177
327
  }
178
- return null;
328
+ return [];
179
329
  }
180
330
  function resolveFormulaExpr(cb) {
181
331
  try {
182
- const cols = new Proxy({}, {
183
- get: (_target, key) => typeof key === "string" ? key : ""
184
- });
185
- return cb(FORMULA_DUMMY_TABLE, cols);
332
+ const cols = new Proxy(
333
+ {},
334
+ {
335
+ get: (_target, key) => typeof key === "string" ? key : ""
336
+ }
337
+ );
338
+ const result = cb(FORMULA_DUMMY_TABLE, cols);
339
+ return typeof result === "string" ? result : String(result);
186
340
  } catch {
187
- return "";
341
+ return UNRESOLVED_FORMULA;
188
342
  }
189
343
  }
190
- function resolveFkType(referencedClassName, metaByClass) {
191
- const refMeta = metaByClass.get(referencedClassName);
192
- if (!refMeta) return "integer";
193
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
194
- return pkProp ? normalizeType(pkProp.type) : "integer";
344
+ function buildForeignKeyColumns(prop, metaByClass) {
345
+ const fieldNames = prop.fieldNames && prop.fieldNames.length > 0 ? prop.fieldNames : [`${prop.name}_id`];
346
+ const fkTypes = resolveFkTypes(prop, metaByClass, fieldNames.length);
347
+ return fieldNames.map((fieldName, index) => ({
348
+ propName: prop.name,
349
+ fieldName,
350
+ type: fkTypes[index] ?? fkTypes[0] ?? "integer",
351
+ isPrimary: prop.primary === true,
352
+ isForeignKey: true,
353
+ isUnique: prop.unique === true,
354
+ isNullable: prop.nullable === true,
355
+ ...prop.comment !== void 0 && { comment: prop.comment },
356
+ referencedEntity: prop.type
357
+ }));
358
+ }
359
+ function resolveFkTypes(prop, metaByClass, fieldNameCount) {
360
+ const refMeta = metaByClass.get(prop.type);
361
+ if (!refMeta) {
362
+ return Array.from({ length: fieldNameCount }, () => "integer");
363
+ }
364
+ const primaryProps = getPrimaryProps(refMeta);
365
+ const referencedColumnNames = prop.referencedColumnNames ?? [];
366
+ return Array.from({ length: fieldNameCount }, (_value, index) => {
367
+ const referencedColumnName = referencedColumnNames[index];
368
+ const pkProp = referencedColumnName !== void 0 ? primaryProps.find((candidate) => candidate.fieldNames.includes(referencedColumnName)) : void 0;
369
+ return (pkProp ?? primaryProps[index] ?? primaryProps[0])?.type ?? "integer";
370
+ });
371
+ }
372
+ function getPrimaryProps(meta) {
373
+ const primaryKeys = meta.primaryKeys ?? [];
374
+ const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
375
+ if (orderedPrimaryProps.length > 0) {
376
+ return orderedPrimaryProps;
377
+ }
378
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
195
379
  }
196
380
  function buildConstraints(meta) {
197
381
  const result = [];
@@ -199,7 +383,7 @@ function buildConstraints(meta) {
199
383
  const props = idx.properties;
200
384
  result.push({
201
385
  type: "index",
202
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
386
+ properties: resolveConstraintProperties(meta, props),
203
387
  ...idx.name !== void 0 && { name: idx.name }
204
388
  });
205
389
  }
@@ -207,12 +391,14 @@ function buildConstraints(meta) {
207
391
  const props = uniq.properties;
208
392
  result.push({
209
393
  type: "unique",
210
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
394
+ properties: resolveConstraintProperties(meta, props),
211
395
  ...uniq.name !== void 0 && { name: uniq.name }
212
396
  });
213
397
  }
214
398
  for (const check of meta.checks ?? []) {
215
- if (typeof check.expression !== "string") continue;
399
+ if (typeof check.expression !== "string") {
400
+ continue;
401
+ }
216
402
  result.push({
217
403
  type: "check",
218
404
  properties: [],
@@ -222,13 +408,30 @@ function buildConstraints(meta) {
222
408
  }
223
409
  return result;
224
410
  }
411
+ function resolveConstraintProperties(meta, props) {
412
+ const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
413
+ return propNames.flatMap((propName) => {
414
+ const prop = meta.properties[String(propName)];
415
+ if (prop === void 0) {
416
+ return [String(propName)];
417
+ }
418
+ if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
419
+ return prop.fieldNames;
420
+ }
421
+ return [prop.name];
422
+ });
423
+ }
225
424
  function buildRelationEdges(metas) {
226
425
  const edges = [];
227
426
  for (const meta of metas) {
228
- if (meta.pivotTable || meta.embeddable) continue;
427
+ if (meta.pivotTable || meta.embeddable) {
428
+ continue;
429
+ }
229
430
  for (const prop of Object.values(meta.properties)) {
230
431
  const edge = buildEdge(meta.className, prop);
231
- if (edge !== null) edges.push(edge);
432
+ if (edge !== null) {
433
+ edges.push(edge);
434
+ }
232
435
  }
233
436
  }
234
437
  return edges;
@@ -236,6 +439,9 @@ function buildRelationEdges(metas) {
236
439
  function buildEdge(fromEntity, prop) {
237
440
  const isNullable = prop.nullable === true;
238
441
  if (prop.kind === ReferenceKind.MANY_TO_ONE) {
442
+ if (prop.type === fromEntity) {
443
+ return null;
444
+ }
239
445
  return {
240
446
  fromEntity,
241
447
  toEntity: prop.type,
@@ -267,7 +473,7 @@ function buildEdge(fromEntity, prop) {
267
473
  function renderErDiagram(model) {
268
474
  const lines = ["erDiagram"];
269
475
  for (const entity of model.entities) {
270
- lines.push(` ${entity.className} {`);
476
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
271
477
  for (const col of entity.columns) {
272
478
  lines.push(` ${renderColumnLine(col)}`);
273
479
  }
@@ -275,16 +481,18 @@ function renderErDiagram(model) {
275
481
  }
276
482
  for (const rel of model.relations) {
277
483
  lines.push(
278
- ` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`
484
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
279
485
  );
280
486
  }
281
487
  return lines.join("\n");
282
488
  }
283
489
  function renderColumnLine(col) {
284
490
  let qualifier = "";
285
- if (col.isPrimary) qualifier = " PK";
286
- else if (col.isForeignKey) qualifier = " FK";
287
- else if (col.isUnique) qualifier = " UK";
491
+ if (col.isPrimary) {
492
+ qualifier = " PK";
493
+ } else if (col.isUnique) {
494
+ qualifier = " UK";
495
+ }
288
496
  let comment;
289
497
  if (col.formula !== void 0) {
290
498
  comment = col.formula ? `formula: ${col.formula}` : "formula";
@@ -292,41 +500,54 @@ function renderColumnLine(col) {
292
500
  comment = "discriminator";
293
501
  } else if (col.embeddedIn !== void 0) {
294
502
  comment = `[${col.embeddedIn}]`;
295
- } else if (col.fieldName !== col.propName) {
296
- comment = col.propName;
503
+ } else if (col.isSelfReference) {
504
+ comment = "self-ref";
297
505
  }
298
- const commentStr = comment !== void 0 ? ` "${comment}"` : "";
299
- return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
300
- }
301
- function normalizeType(type) {
302
- return type.replace(/[^a-zA-Z0-9_]/g, "_");
506
+ const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
507
+ return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
303
508
  }
304
509
 
305
510
  // src/model/build.ts
306
- function buildDocumentModel(metas, jsDocResult, title, description) {
307
- const { entities: diagramEntities, relations: allRelations } = buildDiagramModel(metas);
511
+ var FROM_ONE_OR_MORE = "}|";
512
+ var TO_ONE_OR_MORE = "|{";
513
+ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
514
+ const { entities: diagramEntities, relations } = buildDiagramModel(metas);
515
+ const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
516
+ const hiddenClasses = /* @__PURE__ */ new Set();
517
+ for (const model of diagramEntities) {
518
+ if (jsDocResult.entities.get(model.className)?.hidden) {
519
+ hiddenClasses.add(model.className);
520
+ }
521
+ }
308
522
  const enrichedByClass = /* @__PURE__ */ new Map();
309
523
  for (const model of diagramEntities) {
310
524
  const jsDoc = jsDocResult.entities.get(model.className);
311
- if (jsDoc?.hidden) continue;
312
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
313
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
525
+ if (jsDoc?.hidden) {
526
+ continue;
527
+ }
528
+ const columns = model.columns.filter(
529
+ (col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
530
+ );
531
+ const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
532
+ const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
533
+ const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
534
+ enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
314
535
  }
315
536
  const groupNames = /* @__PURE__ */ new Set();
316
537
  let anyUntagged = false;
317
538
  for (const { jsDoc } of enrichedByClass.values()) {
318
- const allNs = [
319
- ...jsDoc?.namespaces ?? [],
320
- ...jsDoc?.erdNamespaces ?? [],
321
- ...jsDoc?.describeNamespaces ?? []
322
- ];
539
+ const allNs = [...jsDoc?.namespaces ?? [], ...jsDoc?.erdNamespaces ?? [], ...jsDoc?.describeNamespaces ?? []];
323
540
  if (allNs.length === 0) {
324
541
  anyUntagged = true;
325
542
  } else {
326
- for (const ns of allNs) groupNames.add(ns);
543
+ for (const ns of allNs) {
544
+ groupNames.add(ns);
545
+ }
327
546
  }
328
547
  }
329
- if (anyUntagged) groupNames.add("default");
548
+ if (anyUntagged) {
549
+ groupNames.add("default");
550
+ }
330
551
  const groups = [];
331
552
  for (const groupName of groupNames) {
332
553
  const isDefault = groupName === "default";
@@ -337,46 +558,153 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
337
558
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
338
559
  );
339
560
  const erdClassNames = new Set(erdEntities.map((e) => e.model.className));
340
- const erdRelations = allRelations.filter(
341
- (r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity)
342
- );
561
+ const erdRelations = allRelations.filter((r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity));
343
562
  groups.push({ name: groupName, erdEntities, textEntities, erdRelations });
344
563
  }
345
564
  groups.sort((a, b) => {
346
- if (a.name === "default") return 1;
347
- if (b.name === "default") return -1;
565
+ if (a.name === "default") {
566
+ return 1;
567
+ }
568
+ if (b.name === "default") {
569
+ return -1;
570
+ }
348
571
  return a.name.localeCompare(b.name);
349
572
  });
350
573
  return { title, groups, ...description !== void 0 && { description } };
351
574
  }
575
+ function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
576
+ const merged = new Map(ownPropDocs);
577
+ for (const col of columns) {
578
+ if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
579
+ continue;
580
+ }
581
+ const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
582
+ if (info) {
583
+ merged.set(col.propName, info);
584
+ }
585
+ }
586
+ return merged;
587
+ }
588
+ function applyAtLeastOne(relations, metas, props, onWarn) {
589
+ const adjusted = relations.map((edge) => ({ ...edge }));
590
+ const metaByClass = new Map(metas.map((m) => [m.className, m]));
591
+ for (const [className, propMap] of props) {
592
+ const meta = metaByClass.get(className);
593
+ if (!meta) {
594
+ continue;
595
+ }
596
+ for (const [propName, info] of propMap) {
597
+ if (!info.atLeastOne) {
598
+ continue;
599
+ }
600
+ const prop = meta.properties[propName];
601
+ if (!prop) {
602
+ continue;
603
+ }
604
+ let edge;
605
+ if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
606
+ edge = adjusted.find(
607
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
608
+ );
609
+ if (edge) {
610
+ edge.fromCardinality = FROM_ONE_OR_MORE;
611
+ }
612
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
613
+ edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
614
+ if (edge) {
615
+ edge.toCardinality = TO_ONE_OR_MORE;
616
+ }
617
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
618
+ edge = adjusted.find(
619
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
620
+ );
621
+ if (edge) {
622
+ edge.fromCardinality = FROM_ONE_OR_MORE;
623
+ }
624
+ }
625
+ if (!edge) {
626
+ onWarn?.(
627
+ `@atLeastOne on ${className}.${propName} had no effect: no matching relation edge was found. It applies only to collection relations that can be matched to a rendered edge: @OneToMany with mappedBy, or @ManyToMany on either the owning side or an inverse mappedBy side.`
628
+ );
629
+ }
630
+ }
631
+ }
632
+ return adjusted;
633
+ }
352
634
  function hasNoNamespaceTags(jsDoc) {
353
- if (!jsDoc) return true;
635
+ if (!jsDoc) {
636
+ return true;
637
+ }
354
638
  return jsDoc.namespaces.length === 0 && jsDoc.erdNamespaces.length === 0 && jsDoc.describeNamespaces.length === 0;
355
639
  }
356
640
  function belongsToGroupForErd(jsDoc, groupName, isDefault) {
357
- if (isDefault) return hasNoNamespaceTags(jsDoc);
358
- if (!jsDoc) return false;
641
+ if (isDefault) {
642
+ return hasNoNamespaceTags(jsDoc);
643
+ }
644
+ if (!jsDoc) {
645
+ return false;
646
+ }
359
647
  return jsDoc.namespaces.includes(groupName) || jsDoc.erdNamespaces.includes(groupName);
360
648
  }
361
649
  function belongsToGroupForText(jsDoc, groupName, isDefault) {
362
- if (isDefault) return hasNoNamespaceTags(jsDoc);
363
- if (!jsDoc) return false;
650
+ if (isDefault) {
651
+ return hasNoNamespaceTags(jsDoc);
652
+ }
653
+ if (!jsDoc) {
654
+ return false;
655
+ }
364
656
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
365
657
  }
366
658
 
659
+ // src/provider.ts
660
+ async function withTsMorphMetadataProvider(options, onWarn) {
661
+ if (options.metadataProvider !== void 0) {
662
+ return options;
663
+ }
664
+ try {
665
+ const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
666
+ return { ...options, metadataProvider: TsMorphMetadataProvider };
667
+ } catch (err) {
668
+ const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
669
+ const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
670
+ if (!isNotInstalled && onWarn) {
671
+ onWarn(
672
+ `@mikro-orm/reflection is installed but failed to load: ${err instanceof Error ? err.message : String(err)}. Ensure all @mikro-orm/* packages are installed at the same version.`
673
+ );
674
+ }
675
+ return options;
676
+ }
677
+ }
678
+
367
679
  // src/render/markdown.ts
368
680
  function renderMarkdown(docModel) {
369
- const sections = [`# ${docModel.title}`];
681
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
370
682
  if (docModel.description) {
371
- sections.push(docModel.description);
683
+ sections.push(escapeMarkdownParagraph(docModel.description));
684
+ }
685
+ if (docModel.groups.length > 1) {
686
+ sections.push(renderTableOfContents(docModel.groups));
372
687
  }
373
688
  for (const group of docModel.groups) {
374
689
  sections.push(renderGroupSection(group));
375
690
  }
376
691
  return sections.join("\n\n");
377
692
  }
693
+ function renderBulletSection(header, items, renderItem) {
694
+ const lines = [header, ""];
695
+ for (const item of items) {
696
+ lines.push(renderItem(item));
697
+ }
698
+ return lines.join("\n");
699
+ }
700
+ function renderTableOfContents(groups) {
701
+ return renderBulletSection("## Contents", groups, (group) => {
702
+ const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
703
+ return `- [${label}](#${toMarkdownAnchor(group.name)})`;
704
+ });
705
+ }
378
706
  function renderGroupSection(group) {
379
- const parts = [`## ${group.name}`];
707
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
380
708
  if (group.erdEntities.length > 0) {
381
709
  const diagramModel = {
382
710
  entities: group.erdEntities.map((e) => e.model),
@@ -390,16 +718,18 @@ function renderGroupSection(group) {
390
718
  return parts.join("\n\n");
391
719
  }
392
720
  function renderEntitySection(entity) {
393
- const parts = [`### ${entity.model.className}`];
721
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
722
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
394
723
  if (entity.jsDoc?.description) {
395
- parts.push(`> ${entity.jsDoc.description}`);
724
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
396
725
  }
397
726
  if (entity.model.discriminatorColumn) {
727
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
728
+ } else if (entity.model.extendsEntity) {
729
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
398
730
  parts.push(
399
- `*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`
731
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
400
732
  );
401
- } else if (entity.model.extendsEntity) {
402
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
403
733
  }
404
734
  if (entity.model.columns.length > 0) {
405
735
  parts.push(renderColumnTable(entity));
@@ -407,6 +737,10 @@ function renderEntitySection(entity) {
407
737
  if (entity.model.constraints.length > 0) {
408
738
  parts.push(renderConstraints(entity.model.constraints));
409
739
  }
740
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
741
+ if (computedColumns.length > 0) {
742
+ parts.push(renderComputedColumns(computedColumns));
743
+ }
410
744
  return parts.join("\n\n");
411
745
  }
412
746
  function renderColumnTable(entity) {
@@ -415,47 +749,136 @@ function renderColumnTable(entity) {
415
749
  const rows = entity.model.columns.map((col) => {
416
750
  const key = resolveColumnKey(col);
417
751
  const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
418
- const desc = entity.propDocs.get(col.propName)?.description ?? "";
419
- return `| ${col.fieldName} | ${col.type} | ${key} | ${nullable} | ${desc} |`;
752
+ const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
753
+ const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
754
+ const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
755
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(col.type)} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
420
756
  });
421
757
  return [header, sep, ...rows].join("\n");
422
758
  }
423
759
  function resolveColumnKey(col) {
424
- if (col.isPrimary) return "PK";
760
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
761
+ if (col.isPrimary && col.isForeignKey) {
762
+ return `PK, ${fkKey}`;
763
+ }
764
+ if (col.isPrimary) {
765
+ return "PK";
766
+ }
425
767
  if (col.isForeignKey) {
426
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
768
+ return fkKey;
769
+ }
770
+ if (col.isUnique) {
771
+ return "UK";
772
+ }
773
+ if (col.isDiscriminator) {
774
+ return "discriminator";
775
+ }
776
+ if (col.embeddedIn !== void 0) {
777
+ return `[${col.embeddedIn}]`;
427
778
  }
428
- if (col.isUnique) return "UK";
429
- if (col.formula !== void 0) return `formula: ${col.formula}`;
430
- if (col.isDiscriminator) return "discriminator";
431
- if (col.embeddedIn !== void 0) return `[${col.embeddedIn}]`;
432
779
  return "";
433
780
  }
781
+ function renderComputedColumns(columns) {
782
+ return renderBulletSection("**Computed columns:**", columns, (col) => {
783
+ const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
784
+ return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
785
+ });
786
+ }
434
787
  function renderConstraints(constraints) {
435
- const lines = ["**Constraints:**", ""];
436
- for (const c of constraints) {
437
- const name = c.name ? ` \`${c.name}\`` : "";
788
+ return renderBulletSection("**Constraints:**", constraints, (c) => {
789
+ const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
790
+ const properties = c.properties.map(escapeMarkdownInline).join(", ");
438
791
  if (c.type === "index") {
439
- lines.push(`- Index${name}: (${c.properties.join(", ")})`);
440
- } else if (c.type === "unique") {
441
- lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
442
- } else if (c.type === "check") {
443
- lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
792
+ return `- Index${name}: (${properties})`;
444
793
  }
445
- }
446
- return lines.join("\n");
794
+ if (c.type === "unique") {
795
+ return `- Unique${name}: (${properties})`;
796
+ }
797
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
798
+ });
447
799
  }
448
800
 
449
801
  // src/index.ts
802
+ var COMPILED_JS = /\.(c|m)?js$/i;
803
+ function resolveJsDocSources(sourcePaths, src, onWarn) {
804
+ if (src !== void 0 && src.length > 0) {
805
+ return src;
806
+ }
807
+ if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
808
+ onWarn(
809
+ 'Entities were discovered from compiled JavaScript, so JSDoc descriptions and @namespace/@hidden tags cannot be read (build tools strip comments). Hidden entities may be exposed. Pass --src "<glob to your .ts sources>" (or the `src` option) to read JSDoc from the original TypeScript files.'
810
+ );
811
+ }
812
+ return sourcePaths;
813
+ }
814
+ function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
815
+ if (jsDocResult.sourceFileCount === 0) {
816
+ throw new Error(
817
+ `No source files matched the explicit src paths: ${src.join(", ")}
818
+ Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
819
+ );
820
+ }
821
+ const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
822
+ const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
823
+ if (missingConcrete.length > 0) {
824
+ throw new Error(
825
+ `Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
826
+ Check that --src (or the \`src\` option) points at all TypeScript entity files. JSDoc tags such as @namespace and @hidden for missing entities cannot be read.`
827
+ );
828
+ }
829
+ if (onWarn) {
830
+ const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
831
+ if (missingAbstract.length > 0) {
832
+ onWarn(
833
+ `Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
834
+ @hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
835
+ );
836
+ }
837
+ }
838
+ }
839
+ function errorMessages(err) {
840
+ const messages = [];
841
+ const seen = /* @__PURE__ */ new Set();
842
+ let current = err;
843
+ while (current instanceof Error && !seen.has(current)) {
844
+ seen.add(current);
845
+ messages.push(current.message);
846
+ current = current.cause;
847
+ }
848
+ return messages;
849
+ }
850
+ function isMissingTsMorphSourceFile(err) {
851
+ return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
852
+ }
853
+ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
854
+ try {
855
+ return await loadEntityMetadata(effectiveOrm);
856
+ } catch (err) {
857
+ const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
858
+ if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
859
+ throw err;
860
+ }
861
+ try {
862
+ return await loadEntityMetadata(originalOrm);
863
+ } catch {
864
+ throw err;
865
+ }
866
+ }
867
+ }
450
868
  async function generateMarkdown(options) {
451
- const { orm, title = "Database Schema", description, src = [] } = options;
452
- const metas = await loadEntityMetadata(orm);
453
- const jsDocResult = loadJsDoc(src);
454
- const docModel = buildDocumentModel(metas, jsDocResult, title, description);
869
+ const { orm, title = "Database Schema", description, src, onWarn } = options;
870
+ const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
871
+ const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
872
+ const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
873
+ if (src !== void 0 && src.length > 0) {
874
+ assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
875
+ }
876
+ const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
455
877
  return renderMarkdown(docModel);
456
878
  }
457
879
  export {
458
880
  MetadataLoadError,
459
- generateMarkdown
881
+ generateMarkdown,
882
+ resolveJsDocSources
460
883
  };
461
884
  //# sourceMappingURL=index.js.map