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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,69 @@ 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) {
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
+ const resolvedProp = pkProp ?? primaryProps[index] ?? primaryProps[0];
408
+ const rawType = resolvedProp?.type ?? "integer";
409
+ const pkIndex = resolvedProp !== void 0 ? primaryProps.indexOf(resolvedProp) : 0;
410
+ return resolveScalarType(rawType, metaByClass, pkIndex);
411
+ });
412
+ }
413
+ function resolveScalarType(type, metaByClass, pkIndex = 0, depth = 0) {
414
+ if (depth >= 5) {
241
415
  return "integer";
242
416
  }
243
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
244
- return pkProp ? normalizeType(pkProp.type) : "integer";
417
+ const refMeta = metaByClass.get(type);
418
+ if (!refMeta) {
419
+ return type;
420
+ }
421
+ const primaryProps = getPrimaryProps(refMeta);
422
+ if (primaryProps.length === 0) {
423
+ return type;
424
+ }
425
+ const targetProp = primaryProps[pkIndex] ?? primaryProps[0];
426
+ const nextType = targetProp?.type ?? type;
427
+ if (nextType === type) {
428
+ return type;
429
+ }
430
+ return resolveScalarType(nextType, metaByClass, pkIndex, depth + 1);
431
+ }
432
+ function getPrimaryProps(meta) {
433
+ const primaryKeys = meta.primaryKeys ?? [];
434
+ const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
435
+ if (orderedPrimaryProps.length > 0) {
436
+ return orderedPrimaryProps;
437
+ }
438
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
245
439
  }
246
440
  function buildConstraints(meta) {
247
441
  const result = [];
@@ -249,7 +443,7 @@ function buildConstraints(meta) {
249
443
  const props = idx.properties;
250
444
  result.push({
251
445
  type: "index",
252
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
446
+ properties: resolveConstraintProperties(meta, props),
253
447
  ...idx.name !== void 0 && { name: idx.name }
254
448
  });
255
449
  }
@@ -257,7 +451,7 @@ function buildConstraints(meta) {
257
451
  const props = uniq.properties;
258
452
  result.push({
259
453
  type: "unique",
260
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
454
+ properties: resolveConstraintProperties(meta, props),
261
455
  ...uniq.name !== void 0 && { name: uniq.name }
262
456
  });
263
457
  }
@@ -274,6 +468,19 @@ function buildConstraints(meta) {
274
468
  }
275
469
  return result;
276
470
  }
471
+ function resolveConstraintProperties(meta, props) {
472
+ const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
473
+ return propNames.flatMap((propName) => {
474
+ const prop = meta.properties[String(propName)];
475
+ if (prop === void 0) {
476
+ return [String(propName)];
477
+ }
478
+ if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
479
+ return prop.fieldNames;
480
+ }
481
+ return [prop.name];
482
+ });
483
+ }
277
484
  function buildRelationEdges(metas) {
278
485
  const edges = [];
279
486
  for (const meta of metas) {
@@ -292,6 +499,9 @@ function buildRelationEdges(metas) {
292
499
  function buildEdge(fromEntity, prop) {
293
500
  const isNullable = prop.nullable === true;
294
501
  if (prop.kind === import_core2.ReferenceKind.MANY_TO_ONE) {
502
+ if (prop.type === fromEntity) {
503
+ return null;
504
+ }
295
505
  return {
296
506
  fromEntity,
297
507
  toEntity: prop.type,
@@ -320,17 +530,41 @@ function buildEdge(fromEntity, prop) {
320
530
  }
321
531
  return null;
322
532
  }
533
+ function normalizeType(type) {
534
+ const t = type.toLowerCase().trim();
535
+ if (t === "uuid" || t === "text" || t === "string" || t.startsWith("varchar")) {
536
+ return "string";
537
+ }
538
+ if (t === "timestamptz" || t === "timestamp" || t === "datetime") {
539
+ return "datetime";
540
+ }
541
+ if (t === "integer" || t === "int" || t === "bigint" || t === "smallint") {
542
+ return "integer";
543
+ }
544
+ if (t === "doubletype" || t === "double precision" || t === "double" || t === "float" || t === "decimal") {
545
+ return "float";
546
+ }
547
+ if (t === "boolean" || t === "bool") {
548
+ return "boolean";
549
+ }
550
+ if (t === "jsonb") {
551
+ return "json";
552
+ }
553
+ return type;
554
+ }
323
555
  function renderErDiagram(model) {
324
556
  const lines = ["erDiagram"];
325
557
  for (const entity of model.entities) {
326
- lines.push(` ${entity.className} {`);
558
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
327
559
  for (const col of entity.columns) {
328
560
  lines.push(` ${renderColumnLine(col)}`);
329
561
  }
330
562
  lines.push(" }");
331
563
  }
332
564
  for (const rel of model.relations) {
333
- lines.push(` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`);
565
+ lines.push(
566
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
567
+ );
334
568
  }
335
569
  return lines.join("\n");
336
570
  }
@@ -338,8 +572,6 @@ function renderColumnLine(col) {
338
572
  let qualifier = "";
339
573
  if (col.isPrimary) {
340
574
  qualifier = " PK";
341
- } else if (col.isForeignKey) {
342
- qualifier = " FK";
343
575
  } else if (col.isUnique) {
344
576
  qualifier = " UK";
345
577
  }
@@ -350,27 +582,38 @@ function renderColumnLine(col) {
350
582
  comment = "discriminator";
351
583
  } else if (col.embeddedIn !== void 0) {
352
584
  comment = `[${col.embeddedIn}]`;
353
- } else if (col.fieldName !== col.propName) {
354
- comment = col.propName;
585
+ } else if (col.isSelfReference) {
586
+ comment = "self-ref";
355
587
  }
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, "_");
588
+ const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
589
+ return `${toMermaidIdentifier(normalizeType(col.type))} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
361
590
  }
362
591
 
363
592
  // src/model/build.ts
364
- function buildDocumentModel(metas, jsDocResult, title, description) {
365
- const { entities: diagramEntities, relations: allRelations } = buildDiagramModel(metas);
593
+ var FROM_ONE_OR_MORE = "}|";
594
+ var TO_ONE_OR_MORE = "|{";
595
+ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
596
+ const { entities: diagramEntities, relations } = buildDiagramModel(metas);
597
+ const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
598
+ const hiddenClasses = /* @__PURE__ */ new Set();
599
+ for (const model of diagramEntities) {
600
+ if (jsDocResult.entities.get(model.className)?.hidden) {
601
+ hiddenClasses.add(model.className);
602
+ }
603
+ }
366
604
  const enrichedByClass = /* @__PURE__ */ new Map();
367
605
  for (const model of diagramEntities) {
368
606
  const jsDoc = jsDocResult.entities.get(model.className);
369
607
  if (jsDoc?.hidden) {
370
608
  continue;
371
609
  }
372
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
373
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
610
+ const columns = model.columns.filter(
611
+ (col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
612
+ );
613
+ const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
614
+ const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
615
+ const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
616
+ enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
374
617
  }
375
618
  const groupNames = /* @__PURE__ */ new Set();
376
619
  let anyUntagged = false;
@@ -390,9 +633,16 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
390
633
  const groups = [];
391
634
  for (const groupName of groupNames) {
392
635
  const isDefault = groupName === "default";
393
- const erdEntities = [...enrichedByClass.values()].filter(
394
- ({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)
395
- );
636
+ const erdEntities = [...enrichedByClass.values()].filter(({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)).map((entity) => {
637
+ if (isCrossNamespaceInGroup(entity.jsDoc, groupName, isDefault)) {
638
+ const pkColumns = entity.model.columns.filter((col) => col.isPrimary);
639
+ if (pkColumns.length === 0) {
640
+ return null;
641
+ }
642
+ return { ...entity, model: { ...entity.model, columns: pkColumns } };
643
+ }
644
+ return entity;
645
+ }).filter((entity) => entity !== null);
396
646
  const textEntities = [...enrichedByClass.values()].filter(
397
647
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
398
648
  );
@@ -411,6 +661,65 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
411
661
  });
412
662
  return { title, groups, ...description !== void 0 && { description } };
413
663
  }
664
+ function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
665
+ const merged = new Map(ownPropDocs);
666
+ for (const col of columns) {
667
+ if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
668
+ continue;
669
+ }
670
+ const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
671
+ if (info) {
672
+ merged.set(col.propName, info);
673
+ }
674
+ }
675
+ return merged;
676
+ }
677
+ function applyAtLeastOne(relations, metas, props, onWarn) {
678
+ const adjusted = relations.map((edge) => ({ ...edge }));
679
+ const metaByClass = new Map(metas.map((m) => [m.className, m]));
680
+ for (const [className, propMap] of props) {
681
+ const meta = metaByClass.get(className);
682
+ if (!meta) {
683
+ continue;
684
+ }
685
+ for (const [propName, info] of propMap) {
686
+ if (!info.atLeastOne) {
687
+ continue;
688
+ }
689
+ const prop = meta.properties[propName];
690
+ if (!prop) {
691
+ continue;
692
+ }
693
+ let edge;
694
+ if (prop.kind === import_core3.ReferenceKind.ONE_TO_MANY && prop.mappedBy) {
695
+ edge = adjusted.find(
696
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
697
+ );
698
+ if (edge) {
699
+ edge.fromCardinality = FROM_ONE_OR_MORE;
700
+ }
701
+ } else if (prop.kind === import_core3.ReferenceKind.MANY_TO_MANY && prop.owner === true) {
702
+ edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
703
+ if (edge) {
704
+ edge.toCardinality = TO_ONE_OR_MORE;
705
+ }
706
+ } else if (prop.kind === import_core3.ReferenceKind.MANY_TO_MANY && prop.mappedBy) {
707
+ edge = adjusted.find(
708
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
709
+ );
710
+ if (edge) {
711
+ edge.fromCardinality = FROM_ONE_OR_MORE;
712
+ }
713
+ }
714
+ if (!edge) {
715
+ onWarn?.(
716
+ `@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.`
717
+ );
718
+ }
719
+ }
720
+ }
721
+ return adjusted;
722
+ }
414
723
  function hasNoNamespaceTags(jsDoc) {
415
724
  if (!jsDoc) {
416
725
  return true;
@@ -435,20 +744,62 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
435
744
  }
436
745
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
437
746
  }
747
+ function isCrossNamespaceInGroup(jsDoc, groupName, isDefault) {
748
+ if (isDefault || !jsDoc) {
749
+ return false;
750
+ }
751
+ return jsDoc.erdNamespaces.includes(groupName) && !jsDoc.namespaces.includes(groupName) && !jsDoc.describeNamespaces.includes(groupName);
752
+ }
753
+
754
+ // src/provider.ts
755
+ async function withTsMorphMetadataProvider(options, onWarn) {
756
+ if (options.metadataProvider !== void 0) {
757
+ return options;
758
+ }
759
+ try {
760
+ const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
761
+ return { ...options, metadataProvider: TsMorphMetadataProvider };
762
+ } catch (err) {
763
+ const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
764
+ const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
765
+ if (!isNotInstalled && onWarn) {
766
+ onWarn(
767
+ `@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.`
768
+ );
769
+ }
770
+ return options;
771
+ }
772
+ }
438
773
 
439
774
  // src/render/markdown.ts
440
775
  function renderMarkdown(docModel) {
441
- const sections = [`# ${docModel.title}`];
776
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
442
777
  if (docModel.description) {
443
- sections.push(docModel.description);
778
+ sections.push(escapeMarkdownParagraph(docModel.description));
779
+ }
780
+ if (docModel.groups.length > 1) {
781
+ sections.push(renderTableOfContents(docModel.groups));
444
782
  }
445
783
  for (const group of docModel.groups) {
446
784
  sections.push(renderGroupSection(group));
447
785
  }
448
786
  return sections.join("\n\n");
449
787
  }
788
+ function renderBulletSection(header, items, renderItem) {
789
+ const lines = [header, ""];
790
+ for (const item of items) {
791
+ lines.push(renderItem(item));
792
+ }
793
+ return lines.join("\n");
794
+ }
795
+ function renderTableOfContents(groups) {
796
+ return renderBulletSection("## Contents", groups, (group) => {
797
+ const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
798
+ return `- [${label}](#${toMarkdownAnchor(group.name)})`;
799
+ });
800
+ }
450
801
  function renderGroupSection(group) {
451
- const parts = [`## ${group.name}`];
802
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
452
803
  if (group.erdEntities.length > 0) {
453
804
  const diagramModel = {
454
805
  entities: group.erdEntities.map((e) => e.model),
@@ -462,14 +813,18 @@ function renderGroupSection(group) {
462
813
  return parts.join("\n\n");
463
814
  }
464
815
  function renderEntitySection(entity) {
465
- const parts = [`### ${entity.model.className}`];
816
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
817
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
466
818
  if (entity.jsDoc?.description) {
467
- parts.push(`> ${entity.jsDoc.description}`);
819
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
468
820
  }
469
821
  if (entity.model.discriminatorColumn) {
470
- parts.push(`*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`);
822
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
471
823
  } else if (entity.model.extendsEntity) {
472
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
824
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
825
+ parts.push(
826
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
827
+ );
473
828
  }
474
829
  if (entity.model.columns.length > 0) {
475
830
  parts.push(renderColumnTable(entity));
@@ -477,6 +832,10 @@ function renderEntitySection(entity) {
477
832
  if (entity.model.constraints.length > 0) {
478
833
  parts.push(renderConstraints(entity.model.constraints));
479
834
  }
835
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
836
+ if (computedColumns.length > 0) {
837
+ parts.push(renderComputedColumns(computedColumns));
838
+ }
480
839
  return parts.join("\n\n");
481
840
  }
482
841
  function renderColumnTable(entity) {
@@ -485,24 +844,27 @@ function renderColumnTable(entity) {
485
844
  const rows = entity.model.columns.map((col) => {
486
845
  const key = resolveColumnKey(col);
487
846
  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} |`;
847
+ const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
848
+ const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
849
+ const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
850
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(normalizeType(col.type))} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
490
851
  });
491
852
  return [header, sep, ...rows].join("\n");
492
853
  }
493
854
  function resolveColumnKey(col) {
855
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
856
+ if (col.isPrimary && col.isForeignKey) {
857
+ return `PK, ${fkKey}`;
858
+ }
494
859
  if (col.isPrimary) {
495
860
  return "PK";
496
861
  }
497
862
  if (col.isForeignKey) {
498
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
863
+ return fkKey;
499
864
  }
500
865
  if (col.isUnique) {
501
866
  return "UK";
502
867
  }
503
- if (col.formula !== void 0) {
504
- return `formula: ${col.formula}`;
505
- }
506
868
  if (col.isDiscriminator) {
507
869
  return "discriminator";
508
870
  }
@@ -511,32 +873,108 @@ function resolveColumnKey(col) {
511
873
  }
512
874
  return "";
513
875
  }
876
+ function renderComputedColumns(columns) {
877
+ return renderBulletSection("**Computed columns:**", columns, (col) => {
878
+ const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
879
+ return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
880
+ });
881
+ }
514
882
  function renderConstraints(constraints) {
515
- const lines = ["**Constraints:**", ""];
516
- for (const c of constraints) {
517
- const name = c.name ? ` \`${c.name}\`` : "";
883
+ return renderBulletSection("**Constraints:**", constraints, (c) => {
884
+ const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
885
+ const properties = c.properties.map(escapeMarkdownInline).join(", ");
518
886
  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 ?? ""}\``);
887
+ return `- Index${name}: (${properties})`;
524
888
  }
525
- }
526
- return lines.join("\n");
889
+ if (c.type === "unique") {
890
+ return `- Unique${name}: (${properties})`;
891
+ }
892
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
893
+ });
527
894
  }
528
895
 
529
896
  // src/index.ts
897
+ var COMPILED_JS = /\.(c|m)?js$/i;
898
+ function resolveJsDocSources(sourcePaths, src, onWarn) {
899
+ if (src !== void 0 && src.length > 0) {
900
+ return src;
901
+ }
902
+ if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
903
+ onWarn(
904
+ '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.'
905
+ );
906
+ }
907
+ return sourcePaths;
908
+ }
909
+ function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
910
+ if (jsDocResult.sourceFileCount === 0) {
911
+ throw new Error(
912
+ `No source files matched the explicit src paths: ${src.join(", ")}
913
+ Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
914
+ );
915
+ }
916
+ const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
917
+ const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
918
+ if (missingConcrete.length > 0) {
919
+ throw new Error(
920
+ `Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
921
+ 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.`
922
+ );
923
+ }
924
+ if (onWarn) {
925
+ const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
926
+ if (missingAbstract.length > 0) {
927
+ onWarn(
928
+ `Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
929
+ @hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
930
+ );
931
+ }
932
+ }
933
+ }
934
+ function errorMessages(err) {
935
+ const messages = [];
936
+ const seen = /* @__PURE__ */ new Set();
937
+ let current = err;
938
+ while (current instanceof Error && !seen.has(current)) {
939
+ seen.add(current);
940
+ messages.push(current.message);
941
+ current = current.cause;
942
+ }
943
+ return messages;
944
+ }
945
+ function isMissingTsMorphSourceFile(err) {
946
+ return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
947
+ }
948
+ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
949
+ try {
950
+ return await loadEntityMetadata(effectiveOrm);
951
+ } catch (err) {
952
+ const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
953
+ if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
954
+ throw err;
955
+ }
956
+ try {
957
+ return await loadEntityMetadata(originalOrm);
958
+ } catch {
959
+ throw err;
960
+ }
961
+ }
962
+ }
530
963
  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);
964
+ const { orm, title = "Database Schema", description, src, onWarn } = options;
965
+ const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
966
+ const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
967
+ const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
968
+ if (src !== void 0 && src.length > 0) {
969
+ assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
970
+ }
971
+ const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
535
972
  return renderMarkdown(docModel);
536
973
  }
537
974
  // Annotate the CommonJS export names for ESM import in node:
538
975
  0 && (module.exports = {
539
976
  MetadataLoadError,
540
- generateMarkdown
977
+ generateMarkdown,
978
+ resolveJsDocSources
541
979
  });
542
980
  //# sourceMappingURL=index.cjs.map