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.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,47 @@ 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) {
214
- return "integer";
362
+ return Array.from({ length: fieldNameCount }, () => "integer");
363
+ }
364
+ const primaryProps = getPrimaryProps(refMeta);
365
+ const referencedColumnNames = prop.referencedColumnNames ?? [];
366
+ return Array.from({ length: fieldNameCount }, (_value, index) => {
367
+ const referencedColumnName = referencedColumnNames[index];
368
+ const pkProp = referencedColumnName !== void 0 ? primaryProps.find((candidate) => candidate.fieldNames.includes(referencedColumnName)) : void 0;
369
+ return (pkProp ?? primaryProps[index] ?? primaryProps[0])?.type ?? "integer";
370
+ });
371
+ }
372
+ function getPrimaryProps(meta) {
373
+ const primaryKeys = meta.primaryKeys ?? [];
374
+ const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
375
+ if (orderedPrimaryProps.length > 0) {
376
+ return orderedPrimaryProps;
215
377
  }
216
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
217
- return pkProp ? normalizeType(pkProp.type) : "integer";
378
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
218
379
  }
219
380
  function buildConstraints(meta) {
220
381
  const result = [];
@@ -222,7 +383,7 @@ function buildConstraints(meta) {
222
383
  const props = idx.properties;
223
384
  result.push({
224
385
  type: "index",
225
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
386
+ properties: resolveConstraintProperties(meta, props),
226
387
  ...idx.name !== void 0 && { name: idx.name }
227
388
  });
228
389
  }
@@ -230,7 +391,7 @@ function buildConstraints(meta) {
230
391
  const props = uniq.properties;
231
392
  result.push({
232
393
  type: "unique",
233
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
394
+ properties: resolveConstraintProperties(meta, props),
234
395
  ...uniq.name !== void 0 && { name: uniq.name }
235
396
  });
236
397
  }
@@ -247,6 +408,19 @@ function buildConstraints(meta) {
247
408
  }
248
409
  return result;
249
410
  }
411
+ function resolveConstraintProperties(meta, props) {
412
+ const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
413
+ return propNames.flatMap((propName) => {
414
+ const prop = meta.properties[String(propName)];
415
+ if (prop === void 0) {
416
+ return [String(propName)];
417
+ }
418
+ if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
419
+ return prop.fieldNames;
420
+ }
421
+ return [prop.name];
422
+ });
423
+ }
250
424
  function buildRelationEdges(metas) {
251
425
  const edges = [];
252
426
  for (const meta of metas) {
@@ -265,6 +439,9 @@ function buildRelationEdges(metas) {
265
439
  function buildEdge(fromEntity, prop) {
266
440
  const isNullable = prop.nullable === true;
267
441
  if (prop.kind === ReferenceKind.MANY_TO_ONE) {
442
+ if (prop.type === fromEntity) {
443
+ return null;
444
+ }
268
445
  return {
269
446
  fromEntity,
270
447
  toEntity: prop.type,
@@ -296,14 +473,16 @@ function buildEdge(fromEntity, prop) {
296
473
  function renderErDiagram(model) {
297
474
  const lines = ["erDiagram"];
298
475
  for (const entity of model.entities) {
299
- lines.push(` ${entity.className} {`);
476
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
300
477
  for (const col of entity.columns) {
301
478
  lines.push(` ${renderColumnLine(col)}`);
302
479
  }
303
480
  lines.push(" }");
304
481
  }
305
482
  for (const rel of model.relations) {
306
- lines.push(` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`);
483
+ lines.push(
484
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
485
+ );
307
486
  }
308
487
  return lines.join("\n");
309
488
  }
@@ -311,8 +490,6 @@ function renderColumnLine(col) {
311
490
  let qualifier = "";
312
491
  if (col.isPrimary) {
313
492
  qualifier = " PK";
314
- } else if (col.isForeignKey) {
315
- qualifier = " FK";
316
493
  } else if (col.isUnique) {
317
494
  qualifier = " UK";
318
495
  }
@@ -323,27 +500,38 @@ function renderColumnLine(col) {
323
500
  comment = "discriminator";
324
501
  } else if (col.embeddedIn !== void 0) {
325
502
  comment = `[${col.embeddedIn}]`;
326
- } else if (col.fieldName !== col.propName) {
327
- comment = col.propName;
503
+ } else if (col.isSelfReference) {
504
+ comment = "self-ref";
328
505
  }
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, "_");
506
+ const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
507
+ return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
334
508
  }
335
509
 
336
510
  // src/model/build.ts
337
- function buildDocumentModel(metas, jsDocResult, title, description) {
338
- const { entities: diagramEntities, relations: allRelations } = buildDiagramModel(metas);
511
+ var FROM_ONE_OR_MORE = "}|";
512
+ var TO_ONE_OR_MORE = "|{";
513
+ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
514
+ const { entities: diagramEntities, relations } = buildDiagramModel(metas);
515
+ const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
516
+ const hiddenClasses = /* @__PURE__ */ new Set();
517
+ for (const model of diagramEntities) {
518
+ if (jsDocResult.entities.get(model.className)?.hidden) {
519
+ hiddenClasses.add(model.className);
520
+ }
521
+ }
339
522
  const enrichedByClass = /* @__PURE__ */ new Map();
340
523
  for (const model of diagramEntities) {
341
524
  const jsDoc = jsDocResult.entities.get(model.className);
342
525
  if (jsDoc?.hidden) {
343
526
  continue;
344
527
  }
345
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
346
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
528
+ const columns = model.columns.filter(
529
+ (col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
530
+ );
531
+ const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
532
+ const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
533
+ const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
534
+ enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
347
535
  }
348
536
  const groupNames = /* @__PURE__ */ new Set();
349
537
  let anyUntagged = false;
@@ -384,6 +572,65 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
384
572
  });
385
573
  return { title, groups, ...description !== void 0 && { description } };
386
574
  }
575
+ function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
576
+ const merged = new Map(ownPropDocs);
577
+ for (const col of columns) {
578
+ if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
579
+ continue;
580
+ }
581
+ const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
582
+ if (info) {
583
+ merged.set(col.propName, info);
584
+ }
585
+ }
586
+ return merged;
587
+ }
588
+ function applyAtLeastOne(relations, metas, props, onWarn) {
589
+ const adjusted = relations.map((edge) => ({ ...edge }));
590
+ const metaByClass = new Map(metas.map((m) => [m.className, m]));
591
+ for (const [className, propMap] of props) {
592
+ const meta = metaByClass.get(className);
593
+ if (!meta) {
594
+ continue;
595
+ }
596
+ for (const [propName, info] of propMap) {
597
+ if (!info.atLeastOne) {
598
+ continue;
599
+ }
600
+ const prop = meta.properties[propName];
601
+ if (!prop) {
602
+ continue;
603
+ }
604
+ let edge;
605
+ if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
606
+ edge = adjusted.find(
607
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
608
+ );
609
+ if (edge) {
610
+ edge.fromCardinality = FROM_ONE_OR_MORE;
611
+ }
612
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
613
+ edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
614
+ if (edge) {
615
+ edge.toCardinality = TO_ONE_OR_MORE;
616
+ }
617
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
618
+ edge = adjusted.find(
619
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
620
+ );
621
+ if (edge) {
622
+ edge.fromCardinality = FROM_ONE_OR_MORE;
623
+ }
624
+ }
625
+ if (!edge) {
626
+ onWarn?.(
627
+ `@atLeastOne on ${className}.${propName} had no effect: no matching relation edge was found. It applies only to collection relations that can be matched to a rendered edge: @OneToMany with mappedBy, or @ManyToMany on either the owning side or an inverse mappedBy side.`
628
+ );
629
+ }
630
+ }
631
+ }
632
+ return adjusted;
633
+ }
387
634
  function hasNoNamespaceTags(jsDoc) {
388
635
  if (!jsDoc) {
389
636
  return true;
@@ -409,19 +656,55 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
409
656
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
410
657
  }
411
658
 
659
+ // src/provider.ts
660
+ async function withTsMorphMetadataProvider(options, onWarn) {
661
+ if (options.metadataProvider !== void 0) {
662
+ return options;
663
+ }
664
+ try {
665
+ const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
666
+ return { ...options, metadataProvider: TsMorphMetadataProvider };
667
+ } catch (err) {
668
+ const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
669
+ const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
670
+ if (!isNotInstalled && onWarn) {
671
+ onWarn(
672
+ `@mikro-orm/reflection is installed but failed to load: ${err instanceof Error ? err.message : String(err)}. Ensure all @mikro-orm/* packages are installed at the same version.`
673
+ );
674
+ }
675
+ return options;
676
+ }
677
+ }
678
+
412
679
  // src/render/markdown.ts
413
680
  function renderMarkdown(docModel) {
414
- const sections = [`# ${docModel.title}`];
681
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
415
682
  if (docModel.description) {
416
- sections.push(docModel.description);
683
+ sections.push(escapeMarkdownParagraph(docModel.description));
684
+ }
685
+ if (docModel.groups.length > 1) {
686
+ sections.push(renderTableOfContents(docModel.groups));
417
687
  }
418
688
  for (const group of docModel.groups) {
419
689
  sections.push(renderGroupSection(group));
420
690
  }
421
691
  return sections.join("\n\n");
422
692
  }
693
+ function renderBulletSection(header, items, renderItem) {
694
+ const lines = [header, ""];
695
+ for (const item of items) {
696
+ lines.push(renderItem(item));
697
+ }
698
+ return lines.join("\n");
699
+ }
700
+ function renderTableOfContents(groups) {
701
+ return renderBulletSection("## Contents", groups, (group) => {
702
+ const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
703
+ return `- [${label}](#${toMarkdownAnchor(group.name)})`;
704
+ });
705
+ }
423
706
  function renderGroupSection(group) {
424
- const parts = [`## ${group.name}`];
707
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
425
708
  if (group.erdEntities.length > 0) {
426
709
  const diagramModel = {
427
710
  entities: group.erdEntities.map((e) => e.model),
@@ -435,14 +718,18 @@ function renderGroupSection(group) {
435
718
  return parts.join("\n\n");
436
719
  }
437
720
  function renderEntitySection(entity) {
438
- const parts = [`### ${entity.model.className}`];
721
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
722
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
439
723
  if (entity.jsDoc?.description) {
440
- parts.push(`> ${entity.jsDoc.description}`);
724
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
441
725
  }
442
726
  if (entity.model.discriminatorColumn) {
443
- parts.push(`*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`);
727
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
444
728
  } else if (entity.model.extendsEntity) {
445
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
729
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
730
+ parts.push(
731
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
732
+ );
446
733
  }
447
734
  if (entity.model.columns.length > 0) {
448
735
  parts.push(renderColumnTable(entity));
@@ -450,6 +737,10 @@ function renderEntitySection(entity) {
450
737
  if (entity.model.constraints.length > 0) {
451
738
  parts.push(renderConstraints(entity.model.constraints));
452
739
  }
740
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
741
+ if (computedColumns.length > 0) {
742
+ parts.push(renderComputedColumns(computedColumns));
743
+ }
453
744
  return parts.join("\n\n");
454
745
  }
455
746
  function renderColumnTable(entity) {
@@ -458,24 +749,27 @@ function renderColumnTable(entity) {
458
749
  const rows = entity.model.columns.map((col) => {
459
750
  const key = resolveColumnKey(col);
460
751
  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} |`;
752
+ const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
753
+ const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
754
+ const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
755
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(col.type)} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
463
756
  });
464
757
  return [header, sep, ...rows].join("\n");
465
758
  }
466
759
  function resolveColumnKey(col) {
760
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
761
+ if (col.isPrimary && col.isForeignKey) {
762
+ return `PK, ${fkKey}`;
763
+ }
467
764
  if (col.isPrimary) {
468
765
  return "PK";
469
766
  }
470
767
  if (col.isForeignKey) {
471
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
768
+ return fkKey;
472
769
  }
473
770
  if (col.isUnique) {
474
771
  return "UK";
475
772
  }
476
- if (col.formula !== void 0) {
477
- return `formula: ${col.formula}`;
478
- }
479
773
  if (col.isDiscriminator) {
480
774
  return "discriminator";
481
775
  }
@@ -484,31 +778,107 @@ function resolveColumnKey(col) {
484
778
  }
485
779
  return "";
486
780
  }
781
+ function renderComputedColumns(columns) {
782
+ return renderBulletSection("**Computed columns:**", columns, (col) => {
783
+ const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
784
+ return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
785
+ });
786
+ }
487
787
  function renderConstraints(constraints) {
488
- const lines = ["**Constraints:**", ""];
489
- for (const c of constraints) {
490
- const name = c.name ? ` \`${c.name}\`` : "";
788
+ return renderBulletSection("**Constraints:**", constraints, (c) => {
789
+ const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
790
+ const properties = c.properties.map(escapeMarkdownInline).join(", ");
491
791
  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 ?? ""}\``);
792
+ return `- Index${name}: (${properties})`;
497
793
  }
498
- }
499
- return lines.join("\n");
794
+ if (c.type === "unique") {
795
+ return `- Unique${name}: (${properties})`;
796
+ }
797
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
798
+ });
500
799
  }
501
800
 
502
801
  // src/index.ts
802
+ var COMPILED_JS = /\.(c|m)?js$/i;
803
+ function resolveJsDocSources(sourcePaths, src, onWarn) {
804
+ if (src !== void 0 && src.length > 0) {
805
+ return src;
806
+ }
807
+ if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
808
+ onWarn(
809
+ 'Entities were discovered from compiled JavaScript, so JSDoc descriptions and @namespace/@hidden tags cannot be read (build tools strip comments). Hidden entities may be exposed. Pass --src "<glob to your .ts sources>" (or the `src` option) to read JSDoc from the original TypeScript files.'
810
+ );
811
+ }
812
+ return sourcePaths;
813
+ }
814
+ function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
815
+ if (jsDocResult.sourceFileCount === 0) {
816
+ throw new Error(
817
+ `No source files matched the explicit src paths: ${src.join(", ")}
818
+ Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
819
+ );
820
+ }
821
+ const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
822
+ const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
823
+ if (missingConcrete.length > 0) {
824
+ throw new Error(
825
+ `Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
826
+ Check that --src (or the \`src\` option) points at all TypeScript entity files. JSDoc tags such as @namespace and @hidden for missing entities cannot be read.`
827
+ );
828
+ }
829
+ if (onWarn) {
830
+ const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
831
+ if (missingAbstract.length > 0) {
832
+ onWarn(
833
+ `Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
834
+ @hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
835
+ );
836
+ }
837
+ }
838
+ }
839
+ function errorMessages(err) {
840
+ const messages = [];
841
+ const seen = /* @__PURE__ */ new Set();
842
+ let current = err;
843
+ while (current instanceof Error && !seen.has(current)) {
844
+ seen.add(current);
845
+ messages.push(current.message);
846
+ current = current.cause;
847
+ }
848
+ return messages;
849
+ }
850
+ function isMissingTsMorphSourceFile(err) {
851
+ return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
852
+ }
853
+ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
854
+ try {
855
+ return await loadEntityMetadata(effectiveOrm);
856
+ } catch (err) {
857
+ const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
858
+ if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
859
+ throw err;
860
+ }
861
+ try {
862
+ return await loadEntityMetadata(originalOrm);
863
+ } catch {
864
+ throw err;
865
+ }
866
+ }
867
+ }
503
868
  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);
869
+ const { orm, title = "Database Schema", description, src, onWarn } = options;
870
+ const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
871
+ const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
872
+ const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
873
+ if (src !== void 0 && src.length > 0) {
874
+ assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
875
+ }
876
+ const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
508
877
  return renderMarkdown(docModel);
509
878
  }
510
879
  export {
511
880
  MetadataLoadError,
512
- generateMarkdown
881
+ generateMarkdown,
882
+ resolveJsDocSources
513
883
  };
514
884
  //# sourceMappingURL=index.js.map