mikro-orm-markdown 0.1.0-alpha.2 → 0.1.0-alpha.4

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,10 +1,11 @@
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) {
7
- return { entities, props };
6
+ const classNames = /* @__PURE__ */ new Set();
7
+ if (filePaths.length === 0) {
8
+ return { entities, props, sourceFileCount: 0, classNames };
8
9
  }
9
10
  const project = new Project({
10
11
  skipAddingFilesFromTsConfig: true,
@@ -14,34 +15,44 @@ function loadJsDoc(srcGlobs) {
14
15
  skipLibCheck: true
15
16
  }
16
17
  });
17
- project.addSourceFilesAtPaths(srcGlobs);
18
- for (const sourceFile of project.getSourceFiles()) {
19
- for (const cls of sourceFile.getClasses()) {
20
- const className = cls.getName();
21
- if (!className) {
22
- continue;
23
- }
24
- const classDocs = cls.getJsDocs();
25
- if (classDocs.length > 0) {
26
- entities.set(className, parseEntityJsDoc(classDocs));
27
- }
28
- const propMap = /* @__PURE__ */ new Map();
29
- for (const prop of cls.getProperties()) {
30
- const propDocs = prop.getJsDocs();
31
- if (propDocs.length === 0) {
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) {
32
30
  continue;
33
31
  }
34
- const desc = extractDescription(propDocs);
35
- if (desc !== void 0) {
36
- propMap.set(prop.getName(), { description: desc });
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);
37
50
  }
38
51
  }
39
- if (propMap.size > 0) {
40
- props.set(className, propMap);
41
- }
52
+ } catch {
42
53
  }
43
54
  }
44
- return { entities, props };
55
+ return { entities, props, sourceFileCount: sourceFiles.length, classNames };
45
56
  }
46
57
  function parseEntityJsDoc(jsDocs) {
47
58
  const namespaces = [];
@@ -76,18 +87,26 @@ function parseEntityJsDoc(jsDocs) {
76
87
  hidden
77
88
  };
78
89
  }
79
- function extractDescription(jsDocs) {
90
+ function parsePropJsDoc(jsDocs) {
91
+ let description;
92
+ let atLeastOne = false;
80
93
  for (const doc of jsDocs) {
81
94
  const desc = doc.getDescription().trim();
82
- if (desc) {
83
- 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
+ }
84
102
  }
85
103
  }
86
- return void 0;
104
+ return { ...description !== void 0 && { description }, atLeastOne };
87
105
  }
88
106
 
89
107
  // src/metadata/load.ts
90
- import { MikroORM } from "@mikro-orm/core";
108
+ import * as path from "path";
109
+ import { EntitySchema, MikroORM } from "@mikro-orm/core";
91
110
  var MetadataLoadError = class extends Error {
92
111
  constructor(message, cause) {
93
112
  super(message);
@@ -95,13 +114,46 @@ var MetadataLoadError = class extends Error {
95
114
  this.name = "MetadataLoadError";
96
115
  }
97
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
+ }
98
145
  async function loadEntityMetadata(options) {
146
+ assertNoEntitySchemaEntities(options);
99
147
  let orm;
100
148
  try {
101
149
  orm = await MikroORM.init({
102
150
  ...options,
103
151
  debug: false,
104
- connect: 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 }
105
157
  });
106
158
  } catch (cause) {
107
159
  throw new MetadataLoadError(
@@ -109,23 +161,80 @@ async function loadEntityMetadata(options) {
109
161
  cause
110
162
  );
111
163
  }
112
- const all = Object.values(orm.getMetadata().getAll());
113
- if (all.length === 0) {
114
- throw new MetadataLoadError(
115
- "No entities were discovered. Check that your config specifies at least one entity path or class."
116
- );
164
+ try {
165
+ const all = Object.values(orm.getMetadata().getAll());
166
+ if (all.length === 0) {
167
+ throw new MetadataLoadError(
168
+ "No entities were discovered. Check that your config specifies at least one entity path or class."
169
+ );
170
+ }
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 };
174
+ } finally {
175
+ await closeDiscoveryResources(orm);
117
176
  }
118
- return all;
119
177
  }
120
178
 
179
+ // src/model/build.ts
180
+ import { ReferenceKind as ReferenceKind2 } from "@mikro-orm/core";
181
+
121
182
  // src/render/mermaid.ts
122
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
123
231
  var FORMULA_DUMMY_TABLE = {
124
232
  alias: "e0",
125
233
  name: "",
126
234
  qualifiedName: "",
127
235
  toString: () => "e0"
128
236
  };
237
+ var UNRESOLVED_FORMULA = "<unresolved>";
129
238
  function buildDiagramModel(metas) {
130
239
  const metaByClass = new Map(metas.map((m) => [m.className, m]));
131
240
  const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
@@ -133,17 +242,14 @@ function buildDiagramModel(metas) {
133
242
  return { entities, relations };
134
243
  }
135
244
  function buildEntityModel(meta, metaByClass) {
136
- const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.discriminatorValue;
245
+ const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
137
246
  const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
138
247
  const columns = [];
139
248
  for (const prop of Object.values(meta.properties)) {
140
249
  if (isStiRoot && prop.inherited === true) {
141
250
  continue;
142
251
  }
143
- const col = buildColumn(prop, metaByClass, meta);
144
- if (col !== null) {
145
- columns.push(col);
146
- }
252
+ columns.push(...buildColumns(prop, metaByClass, meta));
147
253
  }
148
254
  return {
149
255
  className: meta.className,
@@ -153,47 +259,73 @@ function buildEntityModel(meta, metaByClass) {
153
259
  isEmbeddable: meta.embeddable === true,
154
260
  ...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
155
261
  ...isStiChild && { extendsEntity: meta.extends },
262
+ ...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
156
263
  constraints: buildConstraints(meta)
157
264
  };
158
265
  }
159
- function buildColumn(prop, metaByClass, owningMeta) {
266
+ function buildColumns(prop, metaByClass, owningMeta) {
160
267
  if (prop.kind === ReferenceKind.EMBEDDED) {
161
- return null;
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 [];
162
284
  }
163
285
  if (prop.kind === ReferenceKind.SCALAR) {
286
+ if (prop.object === true && prop.embedded !== void 0) {
287
+ return [];
288
+ }
164
289
  const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
165
290
  let embeddedIn;
291
+ let embeddedPropName;
166
292
  if (prop.embedded !== void 0) {
167
293
  const parentPropName = prop.embedded[0];
168
294
  embeddedIn = owningMeta.properties[parentPropName]?.type;
295
+ embeddedPropName = prop.embedded[1];
169
296
  }
170
297
  const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
171
- return {
172
- propName: prop.name,
173
- fieldName: prop.fieldNames?.[0] ?? prop.name,
174
- type: normalizeType(prop.type),
175
- isPrimary: prop.primary === true,
176
- isForeignKey: false,
177
- isUnique: prop.unique === true,
178
- isNullable: prop.nullable === true,
179
- ...formulaExpr !== void 0 && { formula: formulaExpr },
180
- ...embeddedIn !== void 0 && { embeddedIn },
181
- ...isDiscriminator && { isDiscriminator: true }
182
- };
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
+ ];
183
320
  }
184
321
  if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner === true) {
185
- const fkType = resolveFkType(prop.type, metaByClass);
186
- return {
187
- propName: prop.name,
188
- fieldName: prop.fieldNames?.[0] ?? `${prop.name}_id`,
189
- type: fkType,
190
- isPrimary: false,
191
- isForeignKey: true,
192
- isUnique: prop.unique === true,
193
- isNullable: prop.nullable === true
194
- };
322
+ const cols = buildForeignKeyColumns(prop, metaByClass);
323
+ if (prop.type === owningMeta.className) {
324
+ return cols.map((col) => ({ ...col, isSelfReference: true }));
325
+ }
326
+ return cols;
195
327
  }
196
- return null;
328
+ return [];
197
329
  }
198
330
  function resolveFormulaExpr(cb) {
199
331
  try {
@@ -203,18 +335,69 @@ function resolveFormulaExpr(cb) {
203
335
  get: (_target, key) => typeof key === "string" ? key : ""
204
336
  }
205
337
  );
206
- return cb(FORMULA_DUMMY_TABLE, cols);
338
+ const result = cb(FORMULA_DUMMY_TABLE, cols);
339
+ return typeof result === "string" ? result : String(result);
207
340
  } catch {
208
- return "";
341
+ return UNRESOLVED_FORMULA;
209
342
  }
210
343
  }
211
- function resolveFkType(referencedClassName, metaByClass) {
212
- const refMeta = metaByClass.get(referencedClassName);
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);
213
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
+ const resolvedProp = pkProp ?? primaryProps[index] ?? primaryProps[0];
370
+ const rawType = resolvedProp?.type ?? "integer";
371
+ const pkIndex = resolvedProp !== void 0 ? primaryProps.indexOf(resolvedProp) : 0;
372
+ return resolveScalarType(rawType, metaByClass, pkIndex);
373
+ });
374
+ }
375
+ function resolveScalarType(type, metaByClass, pkIndex = 0, depth = 0) {
376
+ if (depth >= 5) {
214
377
  return "integer";
215
378
  }
216
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
217
- return pkProp ? normalizeType(pkProp.type) : "integer";
379
+ const refMeta = metaByClass.get(type);
380
+ if (!refMeta) {
381
+ return type;
382
+ }
383
+ const primaryProps = getPrimaryProps(refMeta);
384
+ if (primaryProps.length === 0) {
385
+ return type;
386
+ }
387
+ const targetProp = primaryProps[pkIndex] ?? primaryProps[0];
388
+ const nextType = targetProp?.type ?? type;
389
+ if (nextType === type) {
390
+ return type;
391
+ }
392
+ return resolveScalarType(nextType, metaByClass, pkIndex, depth + 1);
393
+ }
394
+ function getPrimaryProps(meta) {
395
+ const primaryKeys = meta.primaryKeys ?? [];
396
+ const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
397
+ if (orderedPrimaryProps.length > 0) {
398
+ return orderedPrimaryProps;
399
+ }
400
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
218
401
  }
219
402
  function buildConstraints(meta) {
220
403
  const result = [];
@@ -222,7 +405,7 @@ function buildConstraints(meta) {
222
405
  const props = idx.properties;
223
406
  result.push({
224
407
  type: "index",
225
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
408
+ properties: resolveConstraintProperties(meta, props),
226
409
  ...idx.name !== void 0 && { name: idx.name }
227
410
  });
228
411
  }
@@ -230,7 +413,7 @@ function buildConstraints(meta) {
230
413
  const props = uniq.properties;
231
414
  result.push({
232
415
  type: "unique",
233
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
416
+ properties: resolveConstraintProperties(meta, props),
234
417
  ...uniq.name !== void 0 && { name: uniq.name }
235
418
  });
236
419
  }
@@ -247,6 +430,19 @@ function buildConstraints(meta) {
247
430
  }
248
431
  return result;
249
432
  }
433
+ function resolveConstraintProperties(meta, props) {
434
+ const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
435
+ return propNames.flatMap((propName) => {
436
+ const prop = meta.properties[String(propName)];
437
+ if (prop === void 0) {
438
+ return [String(propName)];
439
+ }
440
+ if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
441
+ return prop.fieldNames;
442
+ }
443
+ return [prop.name];
444
+ });
445
+ }
250
446
  function buildRelationEdges(metas) {
251
447
  const edges = [];
252
448
  for (const meta of metas) {
@@ -265,6 +461,9 @@ function buildRelationEdges(metas) {
265
461
  function buildEdge(fromEntity, prop) {
266
462
  const isNullable = prop.nullable === true;
267
463
  if (prop.kind === ReferenceKind.MANY_TO_ONE) {
464
+ if (prop.type === fromEntity) {
465
+ return null;
466
+ }
268
467
  return {
269
468
  fromEntity,
270
469
  toEntity: prop.type,
@@ -293,17 +492,41 @@ function buildEdge(fromEntity, prop) {
293
492
  }
294
493
  return null;
295
494
  }
495
+ function normalizeType(type) {
496
+ const t = type.toLowerCase().trim();
497
+ if (t === "uuid" || t === "text" || t === "string" || t.startsWith("varchar")) {
498
+ return "string";
499
+ }
500
+ if (t === "timestamptz" || t === "timestamp" || t === "datetime") {
501
+ return "datetime";
502
+ }
503
+ if (t === "integer" || t === "int" || t === "bigint" || t === "smallint") {
504
+ return "integer";
505
+ }
506
+ if (t === "doubletype" || t === "double precision" || t === "double" || t === "float" || t === "decimal") {
507
+ return "float";
508
+ }
509
+ if (t === "boolean" || t === "bool") {
510
+ return "boolean";
511
+ }
512
+ if (t === "jsonb") {
513
+ return "json";
514
+ }
515
+ return type;
516
+ }
296
517
  function renderErDiagram(model) {
297
518
  const lines = ["erDiagram"];
298
519
  for (const entity of model.entities) {
299
- lines.push(` ${entity.className} {`);
520
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
300
521
  for (const col of entity.columns) {
301
522
  lines.push(` ${renderColumnLine(col)}`);
302
523
  }
303
524
  lines.push(" }");
304
525
  }
305
526
  for (const rel of model.relations) {
306
- lines.push(` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`);
527
+ lines.push(
528
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
529
+ );
307
530
  }
308
531
  return lines.join("\n");
309
532
  }
@@ -311,8 +534,6 @@ function renderColumnLine(col) {
311
534
  let qualifier = "";
312
535
  if (col.isPrimary) {
313
536
  qualifier = " PK";
314
- } else if (col.isForeignKey) {
315
- qualifier = " FK";
316
537
  } else if (col.isUnique) {
317
538
  qualifier = " UK";
318
539
  }
@@ -323,27 +544,38 @@ function renderColumnLine(col) {
323
544
  comment = "discriminator";
324
545
  } else if (col.embeddedIn !== void 0) {
325
546
  comment = `[${col.embeddedIn}]`;
326
- } else if (col.fieldName !== col.propName) {
327
- comment = col.propName;
547
+ } else if (col.isSelfReference) {
548
+ comment = "self-ref";
328
549
  }
329
- const commentStr = comment !== void 0 ? ` "${comment}"` : "";
330
- return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
331
- }
332
- function normalizeType(type) {
333
- return type.replace(/[^a-zA-Z0-9_]/g, "_");
550
+ const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
551
+ return `${toMermaidIdentifier(normalizeType(col.type))} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
334
552
  }
335
553
 
336
554
  // src/model/build.ts
337
- function buildDocumentModel(metas, jsDocResult, title, description) {
338
- const { entities: diagramEntities, relations: allRelations } = buildDiagramModel(metas);
555
+ var FROM_ONE_OR_MORE = "}|";
556
+ var TO_ONE_OR_MORE = "|{";
557
+ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
558
+ const { entities: diagramEntities, relations } = buildDiagramModel(metas);
559
+ const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
560
+ const hiddenClasses = /* @__PURE__ */ new Set();
561
+ for (const model of diagramEntities) {
562
+ if (jsDocResult.entities.get(model.className)?.hidden) {
563
+ hiddenClasses.add(model.className);
564
+ }
565
+ }
339
566
  const enrichedByClass = /* @__PURE__ */ new Map();
340
567
  for (const model of diagramEntities) {
341
568
  const jsDoc = jsDocResult.entities.get(model.className);
342
569
  if (jsDoc?.hidden) {
343
570
  continue;
344
571
  }
345
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
346
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
572
+ const columns = model.columns.filter(
573
+ (col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
574
+ );
575
+ const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
576
+ const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
577
+ const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
578
+ enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
347
579
  }
348
580
  const groupNames = /* @__PURE__ */ new Set();
349
581
  let anyUntagged = false;
@@ -363,9 +595,16 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
363
595
  const groups = [];
364
596
  for (const groupName of groupNames) {
365
597
  const isDefault = groupName === "default";
366
- const erdEntities = [...enrichedByClass.values()].filter(
367
- ({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)
368
- );
598
+ const erdEntities = [...enrichedByClass.values()].filter(({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)).map((entity) => {
599
+ if (isCrossNamespaceInGroup(entity.jsDoc, groupName, isDefault)) {
600
+ const pkColumns = entity.model.columns.filter((col) => col.isPrimary);
601
+ if (pkColumns.length === 0) {
602
+ return null;
603
+ }
604
+ return { ...entity, model: { ...entity.model, columns: pkColumns } };
605
+ }
606
+ return entity;
607
+ }).filter((entity) => entity !== null);
369
608
  const textEntities = [...enrichedByClass.values()].filter(
370
609
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
371
610
  );
@@ -384,6 +623,65 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
384
623
  });
385
624
  return { title, groups, ...description !== void 0 && { description } };
386
625
  }
626
+ function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
627
+ const merged = new Map(ownPropDocs);
628
+ for (const col of columns) {
629
+ if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
630
+ continue;
631
+ }
632
+ const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
633
+ if (info) {
634
+ merged.set(col.propName, info);
635
+ }
636
+ }
637
+ return merged;
638
+ }
639
+ function applyAtLeastOne(relations, metas, props, onWarn) {
640
+ const adjusted = relations.map((edge) => ({ ...edge }));
641
+ const metaByClass = new Map(metas.map((m) => [m.className, m]));
642
+ for (const [className, propMap] of props) {
643
+ const meta = metaByClass.get(className);
644
+ if (!meta) {
645
+ continue;
646
+ }
647
+ for (const [propName, info] of propMap) {
648
+ if (!info.atLeastOne) {
649
+ continue;
650
+ }
651
+ const prop = meta.properties[propName];
652
+ if (!prop) {
653
+ continue;
654
+ }
655
+ let edge;
656
+ if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
657
+ edge = adjusted.find(
658
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
659
+ );
660
+ if (edge) {
661
+ edge.fromCardinality = FROM_ONE_OR_MORE;
662
+ }
663
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
664
+ edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
665
+ if (edge) {
666
+ edge.toCardinality = TO_ONE_OR_MORE;
667
+ }
668
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
669
+ edge = adjusted.find(
670
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
671
+ );
672
+ if (edge) {
673
+ edge.fromCardinality = FROM_ONE_OR_MORE;
674
+ }
675
+ }
676
+ if (!edge) {
677
+ onWarn?.(
678
+ `@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.`
679
+ );
680
+ }
681
+ }
682
+ }
683
+ return adjusted;
684
+ }
387
685
  function hasNoNamespaceTags(jsDoc) {
388
686
  if (!jsDoc) {
389
687
  return true;
@@ -408,20 +706,62 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
408
706
  }
409
707
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
410
708
  }
709
+ function isCrossNamespaceInGroup(jsDoc, groupName, isDefault) {
710
+ if (isDefault || !jsDoc) {
711
+ return false;
712
+ }
713
+ return jsDoc.erdNamespaces.includes(groupName) && !jsDoc.namespaces.includes(groupName) && !jsDoc.describeNamespaces.includes(groupName);
714
+ }
715
+
716
+ // src/provider.ts
717
+ async function withTsMorphMetadataProvider(options, onWarn) {
718
+ if (options.metadataProvider !== void 0) {
719
+ return options;
720
+ }
721
+ try {
722
+ const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
723
+ return { ...options, metadataProvider: TsMorphMetadataProvider };
724
+ } catch (err) {
725
+ const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
726
+ const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
727
+ if (!isNotInstalled && onWarn) {
728
+ onWarn(
729
+ `@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.`
730
+ );
731
+ }
732
+ return options;
733
+ }
734
+ }
411
735
 
412
736
  // src/render/markdown.ts
413
737
  function renderMarkdown(docModel) {
414
- const sections = [`# ${docModel.title}`];
738
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
415
739
  if (docModel.description) {
416
- sections.push(docModel.description);
740
+ sections.push(escapeMarkdownParagraph(docModel.description));
741
+ }
742
+ if (docModel.groups.length > 1) {
743
+ sections.push(renderTableOfContents(docModel.groups));
417
744
  }
418
745
  for (const group of docModel.groups) {
419
746
  sections.push(renderGroupSection(group));
420
747
  }
421
748
  return sections.join("\n\n");
422
749
  }
750
+ function renderBulletSection(header, items, renderItem) {
751
+ const lines = [header, ""];
752
+ for (const item of items) {
753
+ lines.push(renderItem(item));
754
+ }
755
+ return lines.join("\n");
756
+ }
757
+ function renderTableOfContents(groups) {
758
+ return renderBulletSection("## Contents", groups, (group) => {
759
+ const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
760
+ return `- [${label}](#${toMarkdownAnchor(group.name)})`;
761
+ });
762
+ }
423
763
  function renderGroupSection(group) {
424
- const parts = [`## ${group.name}`];
764
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
425
765
  if (group.erdEntities.length > 0) {
426
766
  const diagramModel = {
427
767
  entities: group.erdEntities.map((e) => e.model),
@@ -435,14 +775,18 @@ function renderGroupSection(group) {
435
775
  return parts.join("\n\n");
436
776
  }
437
777
  function renderEntitySection(entity) {
438
- const parts = [`### ${entity.model.className}`];
778
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
779
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
439
780
  if (entity.jsDoc?.description) {
440
- parts.push(`> ${entity.jsDoc.description}`);
781
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
441
782
  }
442
783
  if (entity.model.discriminatorColumn) {
443
- parts.push(`*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`);
784
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
444
785
  } else if (entity.model.extendsEntity) {
445
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
786
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
787
+ parts.push(
788
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
789
+ );
446
790
  }
447
791
  if (entity.model.columns.length > 0) {
448
792
  parts.push(renderColumnTable(entity));
@@ -450,6 +794,10 @@ function renderEntitySection(entity) {
450
794
  if (entity.model.constraints.length > 0) {
451
795
  parts.push(renderConstraints(entity.model.constraints));
452
796
  }
797
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
798
+ if (computedColumns.length > 0) {
799
+ parts.push(renderComputedColumns(computedColumns));
800
+ }
453
801
  return parts.join("\n\n");
454
802
  }
455
803
  function renderColumnTable(entity) {
@@ -458,24 +806,27 @@ function renderColumnTable(entity) {
458
806
  const rows = entity.model.columns.map((col) => {
459
807
  const key = resolveColumnKey(col);
460
808
  const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
461
- const desc = entity.propDocs.get(col.propName)?.description ?? "";
462
- return `| ${col.fieldName} | ${col.type} | ${key} | ${nullable} | ${desc} |`;
809
+ const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
810
+ const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
811
+ const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
812
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(normalizeType(col.type))} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
463
813
  });
464
814
  return [header, sep, ...rows].join("\n");
465
815
  }
466
816
  function resolveColumnKey(col) {
817
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
818
+ if (col.isPrimary && col.isForeignKey) {
819
+ return `PK, ${fkKey}`;
820
+ }
467
821
  if (col.isPrimary) {
468
822
  return "PK";
469
823
  }
470
824
  if (col.isForeignKey) {
471
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
825
+ return fkKey;
472
826
  }
473
827
  if (col.isUnique) {
474
828
  return "UK";
475
829
  }
476
- if (col.formula !== void 0) {
477
- return `formula: ${col.formula}`;
478
- }
479
830
  if (col.isDiscriminator) {
480
831
  return "discriminator";
481
832
  }
@@ -484,31 +835,107 @@ function resolveColumnKey(col) {
484
835
  }
485
836
  return "";
486
837
  }
838
+ function renderComputedColumns(columns) {
839
+ return renderBulletSection("**Computed columns:**", columns, (col) => {
840
+ const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
841
+ return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
842
+ });
843
+ }
487
844
  function renderConstraints(constraints) {
488
- const lines = ["**Constraints:**", ""];
489
- for (const c of constraints) {
490
- const name = c.name ? ` \`${c.name}\`` : "";
845
+ return renderBulletSection("**Constraints:**", constraints, (c) => {
846
+ const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
847
+ const properties = c.properties.map(escapeMarkdownInline).join(", ");
491
848
  if (c.type === "index") {
492
- lines.push(`- Index${name}: (${c.properties.join(", ")})`);
493
- } else if (c.type === "unique") {
494
- lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
495
- } else if (c.type === "check") {
496
- lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
849
+ return `- Index${name}: (${properties})`;
497
850
  }
498
- }
499
- return lines.join("\n");
851
+ if (c.type === "unique") {
852
+ return `- Unique${name}: (${properties})`;
853
+ }
854
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
855
+ });
500
856
  }
501
857
 
502
858
  // src/index.ts
859
+ var COMPILED_JS = /\.(c|m)?js$/i;
860
+ function resolveJsDocSources(sourcePaths, src, onWarn) {
861
+ if (src !== void 0 && src.length > 0) {
862
+ return src;
863
+ }
864
+ if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
865
+ onWarn(
866
+ '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.'
867
+ );
868
+ }
869
+ return sourcePaths;
870
+ }
871
+ function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
872
+ if (jsDocResult.sourceFileCount === 0) {
873
+ throw new Error(
874
+ `No source files matched the explicit src paths: ${src.join(", ")}
875
+ Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
876
+ );
877
+ }
878
+ const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
879
+ const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
880
+ if (missingConcrete.length > 0) {
881
+ throw new Error(
882
+ `Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
883
+ 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.`
884
+ );
885
+ }
886
+ if (onWarn) {
887
+ const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
888
+ if (missingAbstract.length > 0) {
889
+ onWarn(
890
+ `Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
891
+ @hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
892
+ );
893
+ }
894
+ }
895
+ }
896
+ function errorMessages(err) {
897
+ const messages = [];
898
+ const seen = /* @__PURE__ */ new Set();
899
+ let current = err;
900
+ while (current instanceof Error && !seen.has(current)) {
901
+ seen.add(current);
902
+ messages.push(current.message);
903
+ current = current.cause;
904
+ }
905
+ return messages;
906
+ }
907
+ function isMissingTsMorphSourceFile(err) {
908
+ return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
909
+ }
910
+ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
911
+ try {
912
+ return await loadEntityMetadata(effectiveOrm);
913
+ } catch (err) {
914
+ const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
915
+ if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
916
+ throw err;
917
+ }
918
+ try {
919
+ return await loadEntityMetadata(originalOrm);
920
+ } catch {
921
+ throw err;
922
+ }
923
+ }
924
+ }
503
925
  async function generateMarkdown(options) {
504
- const { orm, title = "Database Schema", description, src = [] } = options;
505
- const metas = await loadEntityMetadata(orm);
506
- const jsDocResult = loadJsDoc(src);
507
- const docModel = buildDocumentModel(metas, jsDocResult, title, description);
926
+ const { orm, title = "Database Schema", description, src, onWarn } = options;
927
+ const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
928
+ const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
929
+ const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
930
+ if (src !== void 0 && src.length > 0) {
931
+ assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
932
+ }
933
+ const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
508
934
  return renderMarkdown(docModel);
509
935
  }
510
936
  export {
511
937
  MetadataLoadError,
512
- generateMarkdown
938
+ generateMarkdown,
939
+ resolveJsDocSources
513
940
  };
514
941
  //# sourceMappingURL=index.js.map