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.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,22 +17,34 @@ 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) return { entities, props };
44
+ const classNames = /* @__PURE__ */ new Set();
45
+ if (filePaths.length === 0) {
46
+ return { entities, props, sourceFileCount: 0, classNames };
47
+ }
34
48
  const project = new import_ts_morph.Project({
35
49
  skipAddingFilesFromTsConfig: true,
36
50
  skipLoadingLibFiles: true,
@@ -39,30 +53,44 @@ function loadJsDoc(srcGlobs) {
39
53
  skipLibCheck: true
40
54
  }
41
55
  });
42
- project.addSourceFilesAtPaths(srcGlobs);
43
- for (const sourceFile of project.getSourceFiles()) {
44
- for (const cls of sourceFile.getClasses()) {
45
- const className = cls.getName();
46
- if (!className) continue;
47
- const classDocs = cls.getJsDocs();
48
- if (classDocs.length > 0) {
49
- entities.set(className, parseEntityJsDoc(classDocs));
50
- }
51
- const propMap = /* @__PURE__ */ new Map();
52
- for (const prop of cls.getProperties()) {
53
- const propDocs = prop.getJsDocs();
54
- if (propDocs.length === 0) continue;
55
- const desc = extractDescription(propDocs);
56
- if (desc !== void 0) {
57
- propMap.set(prop.getName(), { description: desc });
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) {
68
+ continue;
69
+ }
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);
58
88
  }
59
89
  }
60
- if (propMap.size > 0) {
61
- props.set(className, propMap);
62
- }
90
+ } catch {
63
91
  }
64
92
  }
65
- return { entities, props };
93
+ return { entities, props, sourceFileCount: sourceFiles.length, classNames };
66
94
  }
67
95
  function parseEntityJsDoc(jsDocs) {
68
96
  const namespaces = [];
@@ -72,14 +100,21 @@ function parseEntityJsDoc(jsDocs) {
72
100
  let description;
73
101
  for (const doc of jsDocs) {
74
102
  const desc = doc.getDescription().trim();
75
- if (desc && description === void 0) description = desc;
103
+ if (desc && description === void 0) {
104
+ description = desc;
105
+ }
76
106
  for (const tag of doc.getTags()) {
77
107
  const tagName = tag.getTagName();
78
108
  const comment = tag.getCommentText()?.trim();
79
- if (tagName === "namespace" && comment) namespaces.push(comment);
80
- else if (tagName === "erd" && comment) erdNamespaces.push(comment);
81
- else if (tagName === "describe" && comment) describeNamespaces.push(comment);
82
- else if (tagName === "hidden") hidden = true;
109
+ if (tagName === "namespace" && comment) {
110
+ namespaces.push(comment);
111
+ } else if (tagName === "erd" && comment) {
112
+ erdNamespaces.push(comment);
113
+ } else if (tagName === "describe" && comment) {
114
+ describeNamespaces.push(comment);
115
+ } else if (tagName === "hidden") {
116
+ hidden = true;
117
+ }
83
118
  }
84
119
  }
85
120
  return {
@@ -90,15 +125,25 @@ function parseEntityJsDoc(jsDocs) {
90
125
  hidden
91
126
  };
92
127
  }
93
- function extractDescription(jsDocs) {
128
+ function parsePropJsDoc(jsDocs) {
129
+ let description;
130
+ let atLeastOne = false;
94
131
  for (const doc of jsDocs) {
95
132
  const desc = doc.getDescription().trim();
96
- if (desc) 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
+ }
140
+ }
97
141
  }
98
- return void 0;
142
+ return { ...description !== void 0 && { description }, atLeastOne };
99
143
  }
100
144
 
101
145
  // src/metadata/load.ts
146
+ var path = __toESM(require("path"), 1);
102
147
  var import_core = require("@mikro-orm/core");
103
148
  var MetadataLoadError = class extends Error {
104
149
  constructor(message, cause) {
@@ -107,12 +152,46 @@ var MetadataLoadError = class extends Error {
107
152
  this.name = "MetadataLoadError";
108
153
  }
109
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
+ }
110
183
  async function loadEntityMetadata(options) {
184
+ assertNoEntitySchemaEntities(options);
111
185
  let orm;
112
186
  try {
113
187
  orm = await import_core.MikroORM.init({
114
188
  ...options,
115
- debug: false
189
+ debug: 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 }
116
195
  });
117
196
  } catch (cause) {
118
197
  throw new MetadataLoadError(
@@ -127,20 +206,73 @@ async function loadEntityMetadata(options) {
127
206
  "No entities were discovered. Check that your config specifies at least one entity path or class."
128
207
  );
129
208
  }
130
- return all;
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 };
131
212
  } finally {
132
- await orm.close(true);
213
+ await closeDiscoveryResources(orm);
133
214
  }
134
215
  }
135
216
 
217
+ // src/model/build.ts
218
+ var import_core3 = require("@mikro-orm/core");
219
+
136
220
  // src/render/mermaid.ts
137
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
138
269
  var FORMULA_DUMMY_TABLE = {
139
270
  alias: "e0",
140
271
  name: "",
141
272
  qualifiedName: "",
142
273
  toString: () => "e0"
143
274
  };
275
+ var UNRESOLVED_FORMULA = "<unresolved>";
144
276
  function buildDiagramModel(metas) {
145
277
  const metaByClass = new Map(metas.map((m) => [m.className, m]));
146
278
  const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
@@ -148,13 +280,14 @@ function buildDiagramModel(metas) {
148
280
  return { entities, relations };
149
281
  }
150
282
  function buildEntityModel(meta, metaByClass) {
151
- const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.discriminatorValue;
283
+ const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
152
284
  const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
153
285
  const columns = [];
154
286
  for (const prop of Object.values(meta.properties)) {
155
- if (isStiRoot && prop.inherited === true) continue;
156
- const col = buildColumn(prop, metaByClass, meta);
157
- if (col !== null) columns.push(col);
287
+ if (isStiRoot && prop.inherited === true) {
288
+ continue;
289
+ }
290
+ columns.push(...buildColumns(prop, metaByClass, meta));
158
291
  }
159
292
  return {
160
293
  className: meta.className,
@@ -164,61 +297,123 @@ function buildEntityModel(meta, metaByClass) {
164
297
  isEmbeddable: meta.embeddable === true,
165
298
  ...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
166
299
  ...isStiChild && { extendsEntity: meta.extends },
300
+ ...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
167
301
  constraints: buildConstraints(meta)
168
302
  };
169
303
  }
170
- function buildColumn(prop, metaByClass, owningMeta) {
171
- if (prop.kind === import_core2.ReferenceKind.EMBEDDED) return null;
304
+ function buildColumns(prop, metaByClass, owningMeta) {
305
+ if (prop.kind === import_core2.ReferenceKind.EMBEDDED) {
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 [];
322
+ }
172
323
  if (prop.kind === import_core2.ReferenceKind.SCALAR) {
324
+ if (prop.object === true && prop.embedded !== void 0) {
325
+ return [];
326
+ }
173
327
  const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
174
328
  let embeddedIn;
329
+ let embeddedPropName;
175
330
  if (prop.embedded !== void 0) {
176
331
  const parentPropName = prop.embedded[0];
177
332
  embeddedIn = owningMeta.properties[parentPropName]?.type;
333
+ embeddedPropName = prop.embedded[1];
178
334
  }
179
335
  const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
180
- return {
181
- propName: prop.name,
182
- fieldName: prop.fieldNames?.[0] ?? prop.name,
183
- type: normalizeType(prop.type),
184
- isPrimary: prop.primary === true,
185
- isForeignKey: false,
186
- isUnique: prop.unique === true,
187
- isNullable: prop.nullable === true,
188
- ...formulaExpr !== void 0 && { formula: formulaExpr },
189
- ...embeddedIn !== void 0 && { embeddedIn },
190
- ...isDiscriminator && { isDiscriminator: true }
191
- };
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
+ ];
192
358
  }
193
359
  if (prop.kind === import_core2.ReferenceKind.MANY_TO_ONE || prop.kind === import_core2.ReferenceKind.ONE_TO_ONE && prop.owner === true) {
194
- const fkType = resolveFkType(prop.type, metaByClass);
195
- return {
196
- propName: prop.name,
197
- fieldName: prop.fieldNames?.[0] ?? `${prop.name}_id`,
198
- type: fkType,
199
- isPrimary: false,
200
- isForeignKey: true,
201
- isUnique: prop.unique === true,
202
- isNullable: prop.nullable === true
203
- };
360
+ const cols = buildForeignKeyColumns(prop, metaByClass);
361
+ if (prop.type === owningMeta.className) {
362
+ return cols.map((col) => ({ ...col, isSelfReference: true }));
363
+ }
364
+ return cols;
204
365
  }
205
- return null;
366
+ return [];
206
367
  }
207
368
  function resolveFormulaExpr(cb) {
208
369
  try {
209
- const cols = new Proxy({}, {
210
- get: (_target, key) => typeof key === "string" ? key : ""
211
- });
212
- return cb(FORMULA_DUMMY_TABLE, cols);
370
+ const cols = new Proxy(
371
+ {},
372
+ {
373
+ get: (_target, key) => typeof key === "string" ? key : ""
374
+ }
375
+ );
376
+ const result = cb(FORMULA_DUMMY_TABLE, cols);
377
+ return typeof result === "string" ? result : String(result);
213
378
  } catch {
214
- return "";
379
+ return UNRESOLVED_FORMULA;
215
380
  }
216
381
  }
217
- function resolveFkType(referencedClassName, metaByClass) {
218
- const refMeta = metaByClass.get(referencedClassName);
219
- if (!refMeta) return "integer";
220
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
221
- return pkProp ? normalizeType(pkProp.type) : "integer";
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);
399
+ if (!refMeta) {
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;
415
+ }
416
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
222
417
  }
223
418
  function buildConstraints(meta) {
224
419
  const result = [];
@@ -226,7 +421,7 @@ function buildConstraints(meta) {
226
421
  const props = idx.properties;
227
422
  result.push({
228
423
  type: "index",
229
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
424
+ properties: resolveConstraintProperties(meta, props),
230
425
  ...idx.name !== void 0 && { name: idx.name }
231
426
  });
232
427
  }
@@ -234,12 +429,14 @@ function buildConstraints(meta) {
234
429
  const props = uniq.properties;
235
430
  result.push({
236
431
  type: "unique",
237
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
432
+ properties: resolveConstraintProperties(meta, props),
238
433
  ...uniq.name !== void 0 && { name: uniq.name }
239
434
  });
240
435
  }
241
436
  for (const check of meta.checks ?? []) {
242
- if (typeof check.expression !== "string") continue;
437
+ if (typeof check.expression !== "string") {
438
+ continue;
439
+ }
243
440
  result.push({
244
441
  type: "check",
245
442
  properties: [],
@@ -249,13 +446,30 @@ function buildConstraints(meta) {
249
446
  }
250
447
  return result;
251
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
+ }
252
462
  function buildRelationEdges(metas) {
253
463
  const edges = [];
254
464
  for (const meta of metas) {
255
- if (meta.pivotTable || meta.embeddable) continue;
465
+ if (meta.pivotTable || meta.embeddable) {
466
+ continue;
467
+ }
256
468
  for (const prop of Object.values(meta.properties)) {
257
469
  const edge = buildEdge(meta.className, prop);
258
- if (edge !== null) edges.push(edge);
470
+ if (edge !== null) {
471
+ edges.push(edge);
472
+ }
259
473
  }
260
474
  }
261
475
  return edges;
@@ -263,6 +477,9 @@ function buildRelationEdges(metas) {
263
477
  function buildEdge(fromEntity, prop) {
264
478
  const isNullable = prop.nullable === true;
265
479
  if (prop.kind === import_core2.ReferenceKind.MANY_TO_ONE) {
480
+ if (prop.type === fromEntity) {
481
+ return null;
482
+ }
266
483
  return {
267
484
  fromEntity,
268
485
  toEntity: prop.type,
@@ -294,7 +511,7 @@ function buildEdge(fromEntity, prop) {
294
511
  function renderErDiagram(model) {
295
512
  const lines = ["erDiagram"];
296
513
  for (const entity of model.entities) {
297
- lines.push(` ${entity.className} {`);
514
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
298
515
  for (const col of entity.columns) {
299
516
  lines.push(` ${renderColumnLine(col)}`);
300
517
  }
@@ -302,16 +519,18 @@ function renderErDiagram(model) {
302
519
  }
303
520
  for (const rel of model.relations) {
304
521
  lines.push(
305
- ` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`
522
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
306
523
  );
307
524
  }
308
525
  return lines.join("\n");
309
526
  }
310
527
  function renderColumnLine(col) {
311
528
  let qualifier = "";
312
- if (col.isPrimary) qualifier = " PK";
313
- else if (col.isForeignKey) qualifier = " FK";
314
- else if (col.isUnique) qualifier = " UK";
529
+ if (col.isPrimary) {
530
+ qualifier = " PK";
531
+ } else if (col.isUnique) {
532
+ qualifier = " UK";
533
+ }
315
534
  let comment;
316
535
  if (col.formula !== void 0) {
317
536
  comment = col.formula ? `formula: ${col.formula}` : "formula";
@@ -319,41 +538,54 @@ function renderColumnLine(col) {
319
538
  comment = "discriminator";
320
539
  } else if (col.embeddedIn !== void 0) {
321
540
  comment = `[${col.embeddedIn}]`;
322
- } else if (col.fieldName !== col.propName) {
323
- comment = col.propName;
541
+ } else if (col.isSelfReference) {
542
+ comment = "self-ref";
324
543
  }
325
- const commentStr = comment !== void 0 ? ` "${comment}"` : "";
326
- return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
327
- }
328
- function normalizeType(type) {
329
- 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}`;
330
546
  }
331
547
 
332
548
  // src/model/build.ts
333
- function buildDocumentModel(metas, jsDocResult, title, description) {
334
- 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
+ }
335
560
  const enrichedByClass = /* @__PURE__ */ new Map();
336
561
  for (const model of diagramEntities) {
337
562
  const jsDoc = jsDocResult.entities.get(model.className);
338
- if (jsDoc?.hidden) continue;
339
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
340
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
563
+ if (jsDoc?.hidden) {
564
+ continue;
565
+ }
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 });
341
573
  }
342
574
  const groupNames = /* @__PURE__ */ new Set();
343
575
  let anyUntagged = false;
344
576
  for (const { jsDoc } of enrichedByClass.values()) {
345
- const allNs = [
346
- ...jsDoc?.namespaces ?? [],
347
- ...jsDoc?.erdNamespaces ?? [],
348
- ...jsDoc?.describeNamespaces ?? []
349
- ];
577
+ const allNs = [...jsDoc?.namespaces ?? [], ...jsDoc?.erdNamespaces ?? [], ...jsDoc?.describeNamespaces ?? []];
350
578
  if (allNs.length === 0) {
351
579
  anyUntagged = true;
352
580
  } else {
353
- for (const ns of allNs) groupNames.add(ns);
581
+ for (const ns of allNs) {
582
+ groupNames.add(ns);
583
+ }
354
584
  }
355
585
  }
356
- if (anyUntagged) groupNames.add("default");
586
+ if (anyUntagged) {
587
+ groupNames.add("default");
588
+ }
357
589
  const groups = [];
358
590
  for (const groupName of groupNames) {
359
591
  const isDefault = groupName === "default";
@@ -364,46 +596,153 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
364
596
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
365
597
  );
366
598
  const erdClassNames = new Set(erdEntities.map((e) => e.model.className));
367
- const erdRelations = allRelations.filter(
368
- (r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity)
369
- );
599
+ const erdRelations = allRelations.filter((r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity));
370
600
  groups.push({ name: groupName, erdEntities, textEntities, erdRelations });
371
601
  }
372
602
  groups.sort((a, b) => {
373
- if (a.name === "default") return 1;
374
- if (b.name === "default") return -1;
603
+ if (a.name === "default") {
604
+ return 1;
605
+ }
606
+ if (b.name === "default") {
607
+ return -1;
608
+ }
375
609
  return a.name.localeCompare(b.name);
376
610
  });
377
611
  return { title, groups, ...description !== void 0 && { description } };
378
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
+ }
379
672
  function hasNoNamespaceTags(jsDoc) {
380
- if (!jsDoc) return true;
673
+ if (!jsDoc) {
674
+ return true;
675
+ }
381
676
  return jsDoc.namespaces.length === 0 && jsDoc.erdNamespaces.length === 0 && jsDoc.describeNamespaces.length === 0;
382
677
  }
383
678
  function belongsToGroupForErd(jsDoc, groupName, isDefault) {
384
- if (isDefault) return hasNoNamespaceTags(jsDoc);
385
- if (!jsDoc) return false;
679
+ if (isDefault) {
680
+ return hasNoNamespaceTags(jsDoc);
681
+ }
682
+ if (!jsDoc) {
683
+ return false;
684
+ }
386
685
  return jsDoc.namespaces.includes(groupName) || jsDoc.erdNamespaces.includes(groupName);
387
686
  }
388
687
  function belongsToGroupForText(jsDoc, groupName, isDefault) {
389
- if (isDefault) return hasNoNamespaceTags(jsDoc);
390
- if (!jsDoc) return false;
688
+ if (isDefault) {
689
+ return hasNoNamespaceTags(jsDoc);
690
+ }
691
+ if (!jsDoc) {
692
+ return false;
693
+ }
391
694
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
392
695
  }
393
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
+
394
717
  // src/render/markdown.ts
395
718
  function renderMarkdown(docModel) {
396
- const sections = [`# ${docModel.title}`];
719
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
397
720
  if (docModel.description) {
398
- sections.push(docModel.description);
721
+ sections.push(escapeMarkdownParagraph(docModel.description));
722
+ }
723
+ if (docModel.groups.length > 1) {
724
+ sections.push(renderTableOfContents(docModel.groups));
399
725
  }
400
726
  for (const group of docModel.groups) {
401
727
  sections.push(renderGroupSection(group));
402
728
  }
403
729
  return sections.join("\n\n");
404
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
+ }
405
744
  function renderGroupSection(group) {
406
- const parts = [`## ${group.name}`];
745
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
407
746
  if (group.erdEntities.length > 0) {
408
747
  const diagramModel = {
409
748
  entities: group.erdEntities.map((e) => e.model),
@@ -417,16 +756,18 @@ function renderGroupSection(group) {
417
756
  return parts.join("\n\n");
418
757
  }
419
758
  function renderEntitySection(entity) {
420
- const parts = [`### ${entity.model.className}`];
759
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
760
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
421
761
  if (entity.jsDoc?.description) {
422
- parts.push(`> ${entity.jsDoc.description}`);
762
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
423
763
  }
424
764
  if (entity.model.discriminatorColumn) {
765
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
766
+ } else if (entity.model.extendsEntity) {
767
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
425
768
  parts.push(
426
- `*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`
769
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
427
770
  );
428
- } else if (entity.model.extendsEntity) {
429
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
430
771
  }
431
772
  if (entity.model.columns.length > 0) {
432
773
  parts.push(renderColumnTable(entity));
@@ -434,6 +775,10 @@ function renderEntitySection(entity) {
434
775
  if (entity.model.constraints.length > 0) {
435
776
  parts.push(renderConstraints(entity.model.constraints));
436
777
  }
778
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
779
+ if (computedColumns.length > 0) {
780
+ parts.push(renderComputedColumns(computedColumns));
781
+ }
437
782
  return parts.join("\n\n");
438
783
  }
439
784
  function renderColumnTable(entity) {
@@ -442,48 +787,137 @@ function renderColumnTable(entity) {
442
787
  const rows = entity.model.columns.map((col) => {
443
788
  const key = resolveColumnKey(col);
444
789
  const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
445
- const desc = entity.propDocs.get(col.propName)?.description ?? "";
446
- 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)} |`;
447
794
  });
448
795
  return [header, sep, ...rows].join("\n");
449
796
  }
450
797
  function resolveColumnKey(col) {
451
- if (col.isPrimary) return "PK";
798
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
799
+ if (col.isPrimary && col.isForeignKey) {
800
+ return `PK, ${fkKey}`;
801
+ }
802
+ if (col.isPrimary) {
803
+ return "PK";
804
+ }
452
805
  if (col.isForeignKey) {
453
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
806
+ return fkKey;
807
+ }
808
+ if (col.isUnique) {
809
+ return "UK";
810
+ }
811
+ if (col.isDiscriminator) {
812
+ return "discriminator";
813
+ }
814
+ if (col.embeddedIn !== void 0) {
815
+ return `[${col.embeddedIn}]`;
454
816
  }
455
- if (col.isUnique) return "UK";
456
- if (col.formula !== void 0) return `formula: ${col.formula}`;
457
- if (col.isDiscriminator) return "discriminator";
458
- if (col.embeddedIn !== void 0) return `[${col.embeddedIn}]`;
459
817
  return "";
460
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
+ }
461
825
  function renderConstraints(constraints) {
462
- const lines = ["**Constraints:**", ""];
463
- for (const c of constraints) {
464
- 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(", ");
465
829
  if (c.type === "index") {
466
- lines.push(`- Index${name}: (${c.properties.join(", ")})`);
467
- } else if (c.type === "unique") {
468
- lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
469
- } else if (c.type === "check") {
470
- lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
830
+ return `- Index${name}: (${properties})`;
471
831
  }
472
- }
473
- return lines.join("\n");
832
+ if (c.type === "unique") {
833
+ return `- Unique${name}: (${properties})`;
834
+ }
835
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
836
+ });
474
837
  }
475
838
 
476
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
+ }
477
906
  async function generateMarkdown(options) {
478
- const { orm, title = "Database Schema", description, src = [] } = options;
479
- const metas = await loadEntityMetadata(orm);
480
- const jsDocResult = loadJsDoc(src);
481
- 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);
482
915
  return renderMarkdown(docModel);
483
916
  }
484
917
  // Annotate the CommonJS export names for ESM import in node:
485
918
  0 && (module.exports = {
486
919
  MetadataLoadError,
487
- generateMarkdown
920
+ generateMarkdown,
921
+ resolveJsDocSources
488
922
  });
489
923
  //# sourceMappingURL=index.cjs.map