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