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/CHANGELOG.md +53 -0
- package/CODE_OF_CONDUCT.md +25 -0
- package/LICENSE +1 -1
- package/README.ko.md +268 -57
- package/README.md +267 -56
- package/SECURITY.md +28 -0
- package/dist/cli.cjs +745 -161
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +51 -0
- package/dist/cli.d.ts +51 -0
- package/dist/cli.js +725 -163
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +577 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -17
- package/dist/index.d.ts +21 -17
- package/dist/index.js +566 -143
- package/dist/index.js.map +1 -1
- package/package.json +44 -16
package/dist/cli.cjs
CHANGED
|
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
9
13
|
var __copyProps = (to, from, except, desc) => {
|
|
10
14
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
15
|
for (let key of __getOwnPropNames(from))
|
|
@@ -22,18 +26,40 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
26
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
27
|
mod
|
|
24
28
|
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
30
|
|
|
26
31
|
// src/cli.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
findNearestTsconfig: () => findNearestTsconfig,
|
|
35
|
+
formatDiscoveryError: () => formatDiscoveryError,
|
|
36
|
+
formatErrorChain: () => formatErrorChain,
|
|
37
|
+
loadOrmOptions: () => loadOrmOptions,
|
|
38
|
+
toConfigImportSpecifier: () => toConfigImportSpecifier,
|
|
39
|
+
writeMarkdownFile: () => writeMarkdownFile
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(cli_exports);
|
|
42
|
+
|
|
43
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
44
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
45
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
46
|
+
|
|
47
|
+
// src/cli.ts
|
|
48
|
+
var syncFs = __toESM(require("fs"), 1);
|
|
27
49
|
var fs = __toESM(require("fs/promises"), 1);
|
|
28
|
-
var
|
|
50
|
+
var path2 = __toESM(require("path"), 1);
|
|
51
|
+
var import_node_url = require("url");
|
|
29
52
|
var import_commander = require("commander");
|
|
30
53
|
|
|
31
54
|
// src/docs/jsdoc.ts
|
|
32
55
|
var import_ts_morph = require("ts-morph");
|
|
33
|
-
function loadJsDoc(
|
|
56
|
+
function loadJsDoc(filePaths) {
|
|
34
57
|
const entities = /* @__PURE__ */ new Map();
|
|
35
58
|
const props = /* @__PURE__ */ new Map();
|
|
36
|
-
|
|
59
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
60
|
+
if (filePaths.length === 0) {
|
|
61
|
+
return { entities, props, sourceFileCount: 0, classNames };
|
|
62
|
+
}
|
|
37
63
|
const project = new import_ts_morph.Project({
|
|
38
64
|
skipAddingFilesFromTsConfig: true,
|
|
39
65
|
skipLoadingLibFiles: true,
|
|
@@ -42,30 +68,44 @@ function loadJsDoc(srcGlobs) {
|
|
|
42
68
|
skipLibCheck: true
|
|
43
69
|
}
|
|
44
70
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
for (const filePath of filePaths) {
|
|
72
|
+
try {
|
|
73
|
+
project.addSourceFilesAtPaths(filePath);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const sourceFiles = project.getSourceFiles();
|
|
78
|
+
for (const sourceFile of sourceFiles) {
|
|
79
|
+
try {
|
|
80
|
+
for (const cls of sourceFile.getClasses()) {
|
|
81
|
+
const className = cls.getName();
|
|
82
|
+
if (!className) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
classNames.add(className);
|
|
86
|
+
const classDocs = cls.getJsDocs();
|
|
87
|
+
if (classDocs.length > 0) {
|
|
88
|
+
entities.set(className, parseEntityJsDoc(classDocs));
|
|
89
|
+
}
|
|
90
|
+
const propMap = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const prop of cls.getProperties()) {
|
|
92
|
+
const propDocs = prop.getJsDocs();
|
|
93
|
+
if (propDocs.length === 0) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const info = parsePropJsDoc(propDocs);
|
|
97
|
+
if (info.description !== void 0 || info.atLeastOne) {
|
|
98
|
+
propMap.set(prop.getName(), info);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (propMap.size > 0) {
|
|
102
|
+
props.set(className, propMap);
|
|
61
103
|
}
|
|
62
104
|
}
|
|
63
|
-
|
|
64
|
-
props.set(className, propMap);
|
|
65
|
-
}
|
|
105
|
+
} catch {
|
|
66
106
|
}
|
|
67
107
|
}
|
|
68
|
-
return { entities, props };
|
|
108
|
+
return { entities, props, sourceFileCount: sourceFiles.length, classNames };
|
|
69
109
|
}
|
|
70
110
|
function parseEntityJsDoc(jsDocs) {
|
|
71
111
|
const namespaces = [];
|
|
@@ -75,14 +115,21 @@ function parseEntityJsDoc(jsDocs) {
|
|
|
75
115
|
let description;
|
|
76
116
|
for (const doc of jsDocs) {
|
|
77
117
|
const desc = doc.getDescription().trim();
|
|
78
|
-
if (desc && description === void 0)
|
|
118
|
+
if (desc && description === void 0) {
|
|
119
|
+
description = desc;
|
|
120
|
+
}
|
|
79
121
|
for (const tag of doc.getTags()) {
|
|
80
122
|
const tagName = tag.getTagName();
|
|
81
123
|
const comment = tag.getCommentText()?.trim();
|
|
82
|
-
if (tagName === "namespace" && comment)
|
|
83
|
-
|
|
84
|
-
else if (tagName === "
|
|
85
|
-
|
|
124
|
+
if (tagName === "namespace" && comment) {
|
|
125
|
+
namespaces.push(comment);
|
|
126
|
+
} else if (tagName === "erd" && comment) {
|
|
127
|
+
erdNamespaces.push(comment);
|
|
128
|
+
} else if (tagName === "describe" && comment) {
|
|
129
|
+
describeNamespaces.push(comment);
|
|
130
|
+
} else if (tagName === "hidden") {
|
|
131
|
+
hidden = true;
|
|
132
|
+
}
|
|
86
133
|
}
|
|
87
134
|
}
|
|
88
135
|
return {
|
|
@@ -93,15 +140,25 @@ function parseEntityJsDoc(jsDocs) {
|
|
|
93
140
|
hidden
|
|
94
141
|
};
|
|
95
142
|
}
|
|
96
|
-
function
|
|
143
|
+
function parsePropJsDoc(jsDocs) {
|
|
144
|
+
let description;
|
|
145
|
+
let atLeastOne = false;
|
|
97
146
|
for (const doc of jsDocs) {
|
|
98
147
|
const desc = doc.getDescription().trim();
|
|
99
|
-
if (desc)
|
|
148
|
+
if (desc && description === void 0) {
|
|
149
|
+
description = desc;
|
|
150
|
+
}
|
|
151
|
+
for (const tag of doc.getTags()) {
|
|
152
|
+
if (tag.getTagName() === "atLeastOne") {
|
|
153
|
+
atLeastOne = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
100
156
|
}
|
|
101
|
-
return void 0;
|
|
157
|
+
return { ...description !== void 0 && { description }, atLeastOne };
|
|
102
158
|
}
|
|
103
159
|
|
|
104
160
|
// src/metadata/load.ts
|
|
161
|
+
var path = __toESM(require("path"), 1);
|
|
105
162
|
var import_core = require("@mikro-orm/core");
|
|
106
163
|
var MetadataLoadError = class extends Error {
|
|
107
164
|
constructor(message, cause) {
|
|
@@ -110,12 +167,46 @@ var MetadataLoadError = class extends Error {
|
|
|
110
167
|
this.name = "MetadataLoadError";
|
|
111
168
|
}
|
|
112
169
|
};
|
|
170
|
+
async function closeDiscoveryResources(orm) {
|
|
171
|
+
await orm.config.getMetadataCacheAdapter()?.close?.();
|
|
172
|
+
await orm.config.getResultCacheAdapter()?.close?.();
|
|
173
|
+
}
|
|
174
|
+
function collectEntitySchemaNames(options) {
|
|
175
|
+
const configuredEntities = [...options.entities ?? [], ...options.entitiesTs ?? []];
|
|
176
|
+
const names = [];
|
|
177
|
+
for (const entity of configuredEntities) {
|
|
178
|
+
if (entity instanceof import_core.EntitySchema) {
|
|
179
|
+
names.push(entity.meta.className);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (entity !== null && typeof entity === "object" && "schema" in entity && entity.schema instanceof import_core.EntitySchema) {
|
|
183
|
+
names.push(entity.schema.meta.className);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return names;
|
|
187
|
+
}
|
|
188
|
+
function assertNoEntitySchemaEntities(options) {
|
|
189
|
+
const schemaNames = collectEntitySchemaNames(options);
|
|
190
|
+
if (schemaNames.length === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
throw new MetadataLoadError(
|
|
194
|
+
`EntitySchema-defined entities are not currently supported: ${schemaNames.join(", ")}.
|
|
195
|
+
Use decorator-based @Entity() classes instead.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
113
198
|
async function loadEntityMetadata(options) {
|
|
199
|
+
assertNoEntitySchemaEntities(options);
|
|
114
200
|
let orm;
|
|
115
201
|
try {
|
|
116
202
|
orm = await import_core.MikroORM.init({
|
|
117
203
|
...options,
|
|
118
|
-
debug: false
|
|
204
|
+
debug: false,
|
|
205
|
+
connect: false,
|
|
206
|
+
// Always disable the metadata cache for one-shot doc runs so the project
|
|
207
|
+
// is never littered with a temp/ folder, regardless of how metadataProvider
|
|
208
|
+
// was configured.
|
|
209
|
+
metadataCache: { ...options.metadataCache, enabled: false }
|
|
119
210
|
});
|
|
120
211
|
} catch (cause) {
|
|
121
212
|
throw new MetadataLoadError(
|
|
@@ -130,20 +221,73 @@ async function loadEntityMetadata(options) {
|
|
|
130
221
|
"No entities were discovered. Check that your config specifies at least one entity path or class."
|
|
131
222
|
);
|
|
132
223
|
}
|
|
133
|
-
|
|
224
|
+
const baseDir = orm.config.get("baseDir");
|
|
225
|
+
const sourcePaths = [...new Set(all.filter((m) => m.path).map((m) => path.resolve(baseDir, m.path)))];
|
|
226
|
+
return { metas: all, sourcePaths };
|
|
134
227
|
} finally {
|
|
135
|
-
await orm
|
|
228
|
+
await closeDiscoveryResources(orm);
|
|
136
229
|
}
|
|
137
230
|
}
|
|
138
231
|
|
|
232
|
+
// src/model/build.ts
|
|
233
|
+
var import_core3 = require("@mikro-orm/core");
|
|
234
|
+
|
|
139
235
|
// src/render/mermaid.ts
|
|
140
236
|
var import_core2 = require("@mikro-orm/core");
|
|
237
|
+
|
|
238
|
+
// src/render/escape.ts
|
|
239
|
+
var MARKDOWN_INLINE_SPECIAL_CHARS = /[\\`|*#]/g;
|
|
240
|
+
var MERMAID_IDENTIFIER_INVALID_CHARS = /[^a-zA-Z0-9_]/g;
|
|
241
|
+
function normalizeInlineText(value) {
|
|
242
|
+
return value.replace(/\r\n?/g, "\n").replace(/[\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
243
|
+
}
|
|
244
|
+
function splitNormalizedLines(value) {
|
|
245
|
+
return value.replace(/\r\n?/g, "\n").split("\n");
|
|
246
|
+
}
|
|
247
|
+
function escapeHtmlText(value) {
|
|
248
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
249
|
+
}
|
|
250
|
+
function escapeMarkdownInline(value) {
|
|
251
|
+
return escapeHtmlText(normalizeInlineText(value)).replace(MARKDOWN_INLINE_SPECIAL_CHARS, "\\$&");
|
|
252
|
+
}
|
|
253
|
+
function escapeMarkdownTableCell(value) {
|
|
254
|
+
return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join("<br>");
|
|
255
|
+
}
|
|
256
|
+
function escapeMarkdownParagraph(value) {
|
|
257
|
+
return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join(" \n");
|
|
258
|
+
}
|
|
259
|
+
function renderMarkdownBlockQuote(value) {
|
|
260
|
+
return splitNormalizedLines(value).map((line) => `> ${escapeMarkdownInline(line)}`).join("\n");
|
|
261
|
+
}
|
|
262
|
+
function renderMarkdownInlineCode(value) {
|
|
263
|
+
const normalized = normalizeInlineText(value);
|
|
264
|
+
const backtickRuns = normalized.match(/`+/g) ?? [];
|
|
265
|
+
const longestRun = Math.max(0, ...backtickRuns.map((run2) => run2.length));
|
|
266
|
+
const fence = "`".repeat(longestRun + 1);
|
|
267
|
+
const needsPadding = normalized.startsWith("`") || normalized.endsWith("`");
|
|
268
|
+
const content = needsPadding ? ` ${normalized} ` : normalized;
|
|
269
|
+
return `${fence}${content}${fence}`;
|
|
270
|
+
}
|
|
271
|
+
function toMermaidIdentifier(value) {
|
|
272
|
+
const normalized = normalizeInlineText(value).replace(MERMAID_IDENTIFIER_INVALID_CHARS, "_").replace(/_+/g, "_");
|
|
273
|
+
const identifier = normalized === "" ? "_" : normalized;
|
|
274
|
+
return /^[a-zA-Z_]/.test(identifier) ? identifier : `_${identifier}`;
|
|
275
|
+
}
|
|
276
|
+
function escapeMermaidQuotedText(value) {
|
|
277
|
+
return normalizeInlineText(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
278
|
+
}
|
|
279
|
+
function toMarkdownAnchor(value) {
|
|
280
|
+
return normalizeInlineText(value).toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/render/mermaid.ts
|
|
141
284
|
var FORMULA_DUMMY_TABLE = {
|
|
142
285
|
alias: "e0",
|
|
143
286
|
name: "",
|
|
144
287
|
qualifiedName: "",
|
|
145
288
|
toString: () => "e0"
|
|
146
289
|
};
|
|
290
|
+
var UNRESOLVED_FORMULA = "<unresolved>";
|
|
147
291
|
function buildDiagramModel(metas) {
|
|
148
292
|
const metaByClass = new Map(metas.map((m) => [m.className, m]));
|
|
149
293
|
const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
|
|
@@ -151,13 +295,14 @@ function buildDiagramModel(metas) {
|
|
|
151
295
|
return { entities, relations };
|
|
152
296
|
}
|
|
153
297
|
function buildEntityModel(meta, metaByClass) {
|
|
154
|
-
const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.
|
|
298
|
+
const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
|
|
155
299
|
const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
|
|
156
300
|
const columns = [];
|
|
157
301
|
for (const prop of Object.values(meta.properties)) {
|
|
158
|
-
if (isStiRoot && prop.inherited === true)
|
|
159
|
-
|
|
160
|
-
|
|
302
|
+
if (isStiRoot && prop.inherited === true) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
columns.push(...buildColumns(prop, metaByClass, meta));
|
|
161
306
|
}
|
|
162
307
|
return {
|
|
163
308
|
className: meta.className,
|
|
@@ -167,61 +312,123 @@ function buildEntityModel(meta, metaByClass) {
|
|
|
167
312
|
isEmbeddable: meta.embeddable === true,
|
|
168
313
|
...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
|
|
169
314
|
...isStiChild && { extendsEntity: meta.extends },
|
|
315
|
+
...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
|
|
170
316
|
constraints: buildConstraints(meta)
|
|
171
317
|
};
|
|
172
318
|
}
|
|
173
|
-
function
|
|
174
|
-
if (prop.kind === import_core2.ReferenceKind.EMBEDDED)
|
|
319
|
+
function buildColumns(prop, metaByClass, owningMeta) {
|
|
320
|
+
if (prop.kind === import_core2.ReferenceKind.EMBEDDED) {
|
|
321
|
+
if (prop.object === true || prop.array === true) {
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
propName: prop.name,
|
|
325
|
+
fieldName: prop.fieldNames?.[0] ?? prop.name,
|
|
326
|
+
type: "json",
|
|
327
|
+
isPrimary: false,
|
|
328
|
+
isForeignKey: false,
|
|
329
|
+
isUnique: prop.unique === true,
|
|
330
|
+
isNullable: prop.nullable === true,
|
|
331
|
+
...prop.comment !== void 0 && { comment: prop.comment },
|
|
332
|
+
embeddedIn: prop.array === true ? `${prop.type}[]` : prop.type
|
|
333
|
+
}
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
175
338
|
if (prop.kind === import_core2.ReferenceKind.SCALAR) {
|
|
339
|
+
if (prop.object === true && prop.embedded !== void 0) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
176
342
|
const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
|
|
177
343
|
let embeddedIn;
|
|
344
|
+
let embeddedPropName;
|
|
178
345
|
if (prop.embedded !== void 0) {
|
|
179
346
|
const parentPropName = prop.embedded[0];
|
|
180
347
|
embeddedIn = owningMeta.properties[parentPropName]?.type;
|
|
348
|
+
embeddedPropName = prop.embedded[1];
|
|
181
349
|
}
|
|
182
350
|
const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
351
|
+
const enumItems = prop.enum === true && Array.isArray(prop.items) && prop.items.length > 0 ? prop.items.map((item) => String(item)) : void 0;
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
propName: prop.name,
|
|
355
|
+
fieldName: prop.fieldNames?.[0] ?? prop.name,
|
|
356
|
+
// Store the original type (e.g. `varchar(255)`); the Mermaid renderer
|
|
357
|
+
// sanitizes it for diagram identifiers, while the markdown table shows
|
|
358
|
+
// it verbatim. Guard against a missing type so downstream string
|
|
359
|
+
// handling never sees undefined (matches the FK path's defaulting).
|
|
360
|
+
type: prop.type ?? "unknown",
|
|
361
|
+
isPrimary: prop.primary === true,
|
|
362
|
+
isForeignKey: false,
|
|
363
|
+
isUnique: prop.unique === true,
|
|
364
|
+
isNullable: prop.nullable === true,
|
|
365
|
+
...prop.comment !== void 0 && { comment: prop.comment },
|
|
366
|
+
...formulaExpr !== void 0 && { formula: formulaExpr },
|
|
367
|
+
...embeddedIn !== void 0 && { embeddedIn },
|
|
368
|
+
...embeddedPropName !== void 0 && { embeddedPropName },
|
|
369
|
+
...isDiscriminator && { isDiscriminator: true },
|
|
370
|
+
...enumItems !== void 0 && { enumItems }
|
|
371
|
+
}
|
|
372
|
+
];
|
|
195
373
|
}
|
|
196
374
|
if (prop.kind === import_core2.ReferenceKind.MANY_TO_ONE || prop.kind === import_core2.ReferenceKind.ONE_TO_ONE && prop.owner === true) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
isPrimary: false,
|
|
203
|
-
isForeignKey: true,
|
|
204
|
-
isUnique: prop.unique === true,
|
|
205
|
-
isNullable: prop.nullable === true
|
|
206
|
-
};
|
|
375
|
+
const cols = buildForeignKeyColumns(prop, metaByClass);
|
|
376
|
+
if (prop.type === owningMeta.className) {
|
|
377
|
+
return cols.map((col) => ({ ...col, isSelfReference: true }));
|
|
378
|
+
}
|
|
379
|
+
return cols;
|
|
207
380
|
}
|
|
208
|
-
return
|
|
381
|
+
return [];
|
|
209
382
|
}
|
|
210
383
|
function resolveFormulaExpr(cb) {
|
|
211
384
|
try {
|
|
212
|
-
const cols = new Proxy(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
385
|
+
const cols = new Proxy(
|
|
386
|
+
{},
|
|
387
|
+
{
|
|
388
|
+
get: (_target, key) => typeof key === "string" ? key : ""
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
const result = cb(FORMULA_DUMMY_TABLE, cols);
|
|
392
|
+
return typeof result === "string" ? result : String(result);
|
|
216
393
|
} catch {
|
|
217
|
-
return
|
|
394
|
+
return UNRESOLVED_FORMULA;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function buildForeignKeyColumns(prop, metaByClass) {
|
|
398
|
+
const fieldNames = prop.fieldNames && prop.fieldNames.length > 0 ? prop.fieldNames : [`${prop.name}_id`];
|
|
399
|
+
const fkTypes = resolveFkTypes(prop, metaByClass, fieldNames.length);
|
|
400
|
+
return fieldNames.map((fieldName, index) => ({
|
|
401
|
+
propName: prop.name,
|
|
402
|
+
fieldName,
|
|
403
|
+
type: fkTypes[index] ?? fkTypes[0] ?? "integer",
|
|
404
|
+
isPrimary: prop.primary === true,
|
|
405
|
+
isForeignKey: true,
|
|
406
|
+
isUnique: prop.unique === true,
|
|
407
|
+
isNullable: prop.nullable === true,
|
|
408
|
+
...prop.comment !== void 0 && { comment: prop.comment },
|
|
409
|
+
referencedEntity: prop.type
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
function resolveFkTypes(prop, metaByClass, fieldNameCount) {
|
|
413
|
+
const refMeta = metaByClass.get(prop.type);
|
|
414
|
+
if (!refMeta) {
|
|
415
|
+
return Array.from({ length: fieldNameCount }, () => "integer");
|
|
218
416
|
}
|
|
417
|
+
const primaryProps = getPrimaryProps(refMeta);
|
|
418
|
+
const referencedColumnNames = prop.referencedColumnNames ?? [];
|
|
419
|
+
return Array.from({ length: fieldNameCount }, (_value, index) => {
|
|
420
|
+
const referencedColumnName = referencedColumnNames[index];
|
|
421
|
+
const pkProp = referencedColumnName !== void 0 ? primaryProps.find((candidate) => candidate.fieldNames.includes(referencedColumnName)) : void 0;
|
|
422
|
+
return (pkProp ?? primaryProps[index] ?? primaryProps[0])?.type ?? "integer";
|
|
423
|
+
});
|
|
219
424
|
}
|
|
220
|
-
function
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
425
|
+
function getPrimaryProps(meta) {
|
|
426
|
+
const primaryKeys = meta.primaryKeys ?? [];
|
|
427
|
+
const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
|
|
428
|
+
if (orderedPrimaryProps.length > 0) {
|
|
429
|
+
return orderedPrimaryProps;
|
|
430
|
+
}
|
|
431
|
+
return Object.values(meta.properties).filter((prop) => prop.primary === true);
|
|
225
432
|
}
|
|
226
433
|
function buildConstraints(meta) {
|
|
227
434
|
const result = [];
|
|
@@ -229,7 +436,7 @@ function buildConstraints(meta) {
|
|
|
229
436
|
const props = idx.properties;
|
|
230
437
|
result.push({
|
|
231
438
|
type: "index",
|
|
232
|
-
properties:
|
|
439
|
+
properties: resolveConstraintProperties(meta, props),
|
|
233
440
|
...idx.name !== void 0 && { name: idx.name }
|
|
234
441
|
});
|
|
235
442
|
}
|
|
@@ -237,12 +444,14 @@ function buildConstraints(meta) {
|
|
|
237
444
|
const props = uniq.properties;
|
|
238
445
|
result.push({
|
|
239
446
|
type: "unique",
|
|
240
|
-
properties:
|
|
447
|
+
properties: resolveConstraintProperties(meta, props),
|
|
241
448
|
...uniq.name !== void 0 && { name: uniq.name }
|
|
242
449
|
});
|
|
243
450
|
}
|
|
244
451
|
for (const check of meta.checks ?? []) {
|
|
245
|
-
if (typeof check.expression !== "string")
|
|
452
|
+
if (typeof check.expression !== "string") {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
246
455
|
result.push({
|
|
247
456
|
type: "check",
|
|
248
457
|
properties: [],
|
|
@@ -252,13 +461,30 @@ function buildConstraints(meta) {
|
|
|
252
461
|
}
|
|
253
462
|
return result;
|
|
254
463
|
}
|
|
464
|
+
function resolveConstraintProperties(meta, props) {
|
|
465
|
+
const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
|
|
466
|
+
return propNames.flatMap((propName) => {
|
|
467
|
+
const prop = meta.properties[String(propName)];
|
|
468
|
+
if (prop === void 0) {
|
|
469
|
+
return [String(propName)];
|
|
470
|
+
}
|
|
471
|
+
if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
|
|
472
|
+
return prop.fieldNames;
|
|
473
|
+
}
|
|
474
|
+
return [prop.name];
|
|
475
|
+
});
|
|
476
|
+
}
|
|
255
477
|
function buildRelationEdges(metas) {
|
|
256
478
|
const edges = [];
|
|
257
479
|
for (const meta of metas) {
|
|
258
|
-
if (meta.pivotTable || meta.embeddable)
|
|
480
|
+
if (meta.pivotTable || meta.embeddable) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
259
483
|
for (const prop of Object.values(meta.properties)) {
|
|
260
484
|
const edge = buildEdge(meta.className, prop);
|
|
261
|
-
if (edge !== null)
|
|
485
|
+
if (edge !== null) {
|
|
486
|
+
edges.push(edge);
|
|
487
|
+
}
|
|
262
488
|
}
|
|
263
489
|
}
|
|
264
490
|
return edges;
|
|
@@ -266,6 +492,9 @@ function buildRelationEdges(metas) {
|
|
|
266
492
|
function buildEdge(fromEntity, prop) {
|
|
267
493
|
const isNullable = prop.nullable === true;
|
|
268
494
|
if (prop.kind === import_core2.ReferenceKind.MANY_TO_ONE) {
|
|
495
|
+
if (prop.type === fromEntity) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
269
498
|
return {
|
|
270
499
|
fromEntity,
|
|
271
500
|
toEntity: prop.type,
|
|
@@ -297,7 +526,7 @@ function buildEdge(fromEntity, prop) {
|
|
|
297
526
|
function renderErDiagram(model) {
|
|
298
527
|
const lines = ["erDiagram"];
|
|
299
528
|
for (const entity of model.entities) {
|
|
300
|
-
lines.push(` ${entity.className} {`);
|
|
529
|
+
lines.push(` ${toMermaidIdentifier(entity.className)} {`);
|
|
301
530
|
for (const col of entity.columns) {
|
|
302
531
|
lines.push(` ${renderColumnLine(col)}`);
|
|
303
532
|
}
|
|
@@ -305,16 +534,18 @@ function renderErDiagram(model) {
|
|
|
305
534
|
}
|
|
306
535
|
for (const rel of model.relations) {
|
|
307
536
|
lines.push(
|
|
308
|
-
` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`
|
|
537
|
+
` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
|
|
309
538
|
);
|
|
310
539
|
}
|
|
311
540
|
return lines.join("\n");
|
|
312
541
|
}
|
|
313
542
|
function renderColumnLine(col) {
|
|
314
543
|
let qualifier = "";
|
|
315
|
-
if (col.isPrimary)
|
|
316
|
-
|
|
317
|
-
else if (col.isUnique)
|
|
544
|
+
if (col.isPrimary) {
|
|
545
|
+
qualifier = " PK";
|
|
546
|
+
} else if (col.isUnique) {
|
|
547
|
+
qualifier = " UK";
|
|
548
|
+
}
|
|
318
549
|
let comment;
|
|
319
550
|
if (col.formula !== void 0) {
|
|
320
551
|
comment = col.formula ? `formula: ${col.formula}` : "formula";
|
|
@@ -322,41 +553,54 @@ function renderColumnLine(col) {
|
|
|
322
553
|
comment = "discriminator";
|
|
323
554
|
} else if (col.embeddedIn !== void 0) {
|
|
324
555
|
comment = `[${col.embeddedIn}]`;
|
|
325
|
-
} else if (col.
|
|
326
|
-
comment =
|
|
556
|
+
} else if (col.isSelfReference) {
|
|
557
|
+
comment = "self-ref";
|
|
327
558
|
}
|
|
328
|
-
const commentStr = comment !== void 0 ? ` "${comment}"` : "";
|
|
329
|
-
return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
|
|
330
|
-
}
|
|
331
|
-
function normalizeType(type) {
|
|
332
|
-
return type.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
559
|
+
const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
|
|
560
|
+
return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
|
|
333
561
|
}
|
|
334
562
|
|
|
335
563
|
// src/model/build.ts
|
|
336
|
-
|
|
337
|
-
|
|
564
|
+
var FROM_ONE_OR_MORE = "}|";
|
|
565
|
+
var TO_ONE_OR_MORE = "|{";
|
|
566
|
+
function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
|
|
567
|
+
const { entities: diagramEntities, relations } = buildDiagramModel(metas);
|
|
568
|
+
const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
|
|
569
|
+
const hiddenClasses = /* @__PURE__ */ new Set();
|
|
570
|
+
for (const model of diagramEntities) {
|
|
571
|
+
if (jsDocResult.entities.get(model.className)?.hidden) {
|
|
572
|
+
hiddenClasses.add(model.className);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
338
575
|
const enrichedByClass = /* @__PURE__ */ new Map();
|
|
339
576
|
for (const model of diagramEntities) {
|
|
340
577
|
const jsDoc = jsDocResult.entities.get(model.className);
|
|
341
|
-
if (jsDoc?.hidden)
|
|
342
|
-
|
|
343
|
-
|
|
578
|
+
if (jsDoc?.hidden) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const columns = model.columns.filter(
|
|
582
|
+
(col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
|
|
583
|
+
);
|
|
584
|
+
const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
|
|
585
|
+
const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
|
|
586
|
+
const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
|
|
587
|
+
enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
|
|
344
588
|
}
|
|
345
589
|
const groupNames = /* @__PURE__ */ new Set();
|
|
346
590
|
let anyUntagged = false;
|
|
347
591
|
for (const { jsDoc } of enrichedByClass.values()) {
|
|
348
|
-
const allNs = [
|
|
349
|
-
...jsDoc?.namespaces ?? [],
|
|
350
|
-
...jsDoc?.erdNamespaces ?? [],
|
|
351
|
-
...jsDoc?.describeNamespaces ?? []
|
|
352
|
-
];
|
|
592
|
+
const allNs = [...jsDoc?.namespaces ?? [], ...jsDoc?.erdNamespaces ?? [], ...jsDoc?.describeNamespaces ?? []];
|
|
353
593
|
if (allNs.length === 0) {
|
|
354
594
|
anyUntagged = true;
|
|
355
595
|
} else {
|
|
356
|
-
for (const ns of allNs)
|
|
596
|
+
for (const ns of allNs) {
|
|
597
|
+
groupNames.add(ns);
|
|
598
|
+
}
|
|
357
599
|
}
|
|
358
600
|
}
|
|
359
|
-
if (anyUntagged)
|
|
601
|
+
if (anyUntagged) {
|
|
602
|
+
groupNames.add("default");
|
|
603
|
+
}
|
|
360
604
|
const groups = [];
|
|
361
605
|
for (const groupName of groupNames) {
|
|
362
606
|
const isDefault = groupName === "default";
|
|
@@ -367,46 +611,153 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
|
|
|
367
611
|
({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
|
|
368
612
|
);
|
|
369
613
|
const erdClassNames = new Set(erdEntities.map((e) => e.model.className));
|
|
370
|
-
const erdRelations = allRelations.filter(
|
|
371
|
-
(r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity)
|
|
372
|
-
);
|
|
614
|
+
const erdRelations = allRelations.filter((r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity));
|
|
373
615
|
groups.push({ name: groupName, erdEntities, textEntities, erdRelations });
|
|
374
616
|
}
|
|
375
617
|
groups.sort((a, b) => {
|
|
376
|
-
if (a.name === "default")
|
|
377
|
-
|
|
618
|
+
if (a.name === "default") {
|
|
619
|
+
return 1;
|
|
620
|
+
}
|
|
621
|
+
if (b.name === "default") {
|
|
622
|
+
return -1;
|
|
623
|
+
}
|
|
378
624
|
return a.name.localeCompare(b.name);
|
|
379
625
|
});
|
|
380
626
|
return { title, groups, ...description !== void 0 && { description } };
|
|
381
627
|
}
|
|
628
|
+
function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
|
|
629
|
+
const merged = new Map(ownPropDocs);
|
|
630
|
+
for (const col of columns) {
|
|
631
|
+
if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
|
|
635
|
+
if (info) {
|
|
636
|
+
merged.set(col.propName, info);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return merged;
|
|
640
|
+
}
|
|
641
|
+
function applyAtLeastOne(relations, metas, props, onWarn) {
|
|
642
|
+
const adjusted = relations.map((edge) => ({ ...edge }));
|
|
643
|
+
const metaByClass = new Map(metas.map((m) => [m.className, m]));
|
|
644
|
+
for (const [className, propMap] of props) {
|
|
645
|
+
const meta = metaByClass.get(className);
|
|
646
|
+
if (!meta) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
for (const [propName, info] of propMap) {
|
|
650
|
+
if (!info.atLeastOne) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const prop = meta.properties[propName];
|
|
654
|
+
if (!prop) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
let edge;
|
|
658
|
+
if (prop.kind === import_core3.ReferenceKind.ONE_TO_MANY && prop.mappedBy) {
|
|
659
|
+
edge = adjusted.find(
|
|
660
|
+
(e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
|
|
661
|
+
);
|
|
662
|
+
if (edge) {
|
|
663
|
+
edge.fromCardinality = FROM_ONE_OR_MORE;
|
|
664
|
+
}
|
|
665
|
+
} else if (prop.kind === import_core3.ReferenceKind.MANY_TO_MANY && prop.owner === true) {
|
|
666
|
+
edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
|
|
667
|
+
if (edge) {
|
|
668
|
+
edge.toCardinality = TO_ONE_OR_MORE;
|
|
669
|
+
}
|
|
670
|
+
} else if (prop.kind === import_core3.ReferenceKind.MANY_TO_MANY && prop.mappedBy) {
|
|
671
|
+
edge = adjusted.find(
|
|
672
|
+
(e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
|
|
673
|
+
);
|
|
674
|
+
if (edge) {
|
|
675
|
+
edge.fromCardinality = FROM_ONE_OR_MORE;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (!edge) {
|
|
679
|
+
onWarn?.(
|
|
680
|
+
`@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.`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return adjusted;
|
|
686
|
+
}
|
|
382
687
|
function hasNoNamespaceTags(jsDoc) {
|
|
383
|
-
if (!jsDoc)
|
|
688
|
+
if (!jsDoc) {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
384
691
|
return jsDoc.namespaces.length === 0 && jsDoc.erdNamespaces.length === 0 && jsDoc.describeNamespaces.length === 0;
|
|
385
692
|
}
|
|
386
693
|
function belongsToGroupForErd(jsDoc, groupName, isDefault) {
|
|
387
|
-
if (isDefault)
|
|
388
|
-
|
|
694
|
+
if (isDefault) {
|
|
695
|
+
return hasNoNamespaceTags(jsDoc);
|
|
696
|
+
}
|
|
697
|
+
if (!jsDoc) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
389
700
|
return jsDoc.namespaces.includes(groupName) || jsDoc.erdNamespaces.includes(groupName);
|
|
390
701
|
}
|
|
391
702
|
function belongsToGroupForText(jsDoc, groupName, isDefault) {
|
|
392
|
-
if (isDefault)
|
|
393
|
-
|
|
703
|
+
if (isDefault) {
|
|
704
|
+
return hasNoNamespaceTags(jsDoc);
|
|
705
|
+
}
|
|
706
|
+
if (!jsDoc) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
394
709
|
return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
|
|
395
710
|
}
|
|
396
711
|
|
|
712
|
+
// src/provider.ts
|
|
713
|
+
async function withTsMorphMetadataProvider(options, onWarn) {
|
|
714
|
+
if (options.metadataProvider !== void 0) {
|
|
715
|
+
return options;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
|
|
719
|
+
return { ...options, metadataProvider: TsMorphMetadataProvider };
|
|
720
|
+
} catch (err) {
|
|
721
|
+
const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
722
|
+
const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
|
|
723
|
+
if (!isNotInstalled && onWarn) {
|
|
724
|
+
onWarn(
|
|
725
|
+
`@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.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return options;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
397
732
|
// src/render/markdown.ts
|
|
398
733
|
function renderMarkdown(docModel) {
|
|
399
|
-
const sections = [`# ${docModel.title}`];
|
|
734
|
+
const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
|
|
400
735
|
if (docModel.description) {
|
|
401
|
-
sections.push(docModel.description);
|
|
736
|
+
sections.push(escapeMarkdownParagraph(docModel.description));
|
|
737
|
+
}
|
|
738
|
+
if (docModel.groups.length > 1) {
|
|
739
|
+
sections.push(renderTableOfContents(docModel.groups));
|
|
402
740
|
}
|
|
403
741
|
for (const group of docModel.groups) {
|
|
404
742
|
sections.push(renderGroupSection(group));
|
|
405
743
|
}
|
|
406
744
|
return sections.join("\n\n");
|
|
407
745
|
}
|
|
746
|
+
function renderBulletSection(header, items, renderItem) {
|
|
747
|
+
const lines = [header, ""];
|
|
748
|
+
for (const item of items) {
|
|
749
|
+
lines.push(renderItem(item));
|
|
750
|
+
}
|
|
751
|
+
return lines.join("\n");
|
|
752
|
+
}
|
|
753
|
+
function renderTableOfContents(groups) {
|
|
754
|
+
return renderBulletSection("## Contents", groups, (group) => {
|
|
755
|
+
const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
|
|
756
|
+
return `- [${label}](#${toMarkdownAnchor(group.name)})`;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
408
759
|
function renderGroupSection(group) {
|
|
409
|
-
const parts = [`## ${group.name}`];
|
|
760
|
+
const parts = [`## ${escapeMarkdownInline(group.name)}`];
|
|
410
761
|
if (group.erdEntities.length > 0) {
|
|
411
762
|
const diagramModel = {
|
|
412
763
|
entities: group.erdEntities.map((e) => e.model),
|
|
@@ -420,16 +771,18 @@ function renderGroupSection(group) {
|
|
|
420
771
|
return parts.join("\n\n");
|
|
421
772
|
}
|
|
422
773
|
function renderEntitySection(entity) {
|
|
423
|
-
const parts = [`### ${entity.model.className}`];
|
|
774
|
+
const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
|
|
775
|
+
parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
|
|
424
776
|
if (entity.jsDoc?.description) {
|
|
425
|
-
parts.push(
|
|
777
|
+
parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
|
|
426
778
|
}
|
|
427
779
|
if (entity.model.discriminatorColumn) {
|
|
780
|
+
parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
|
|
781
|
+
} else if (entity.model.extendsEntity) {
|
|
782
|
+
const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
|
|
428
783
|
parts.push(
|
|
429
|
-
`*
|
|
784
|
+
`*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
|
|
430
785
|
);
|
|
431
|
-
} else if (entity.model.extendsEntity) {
|
|
432
|
-
parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
|
|
433
786
|
}
|
|
434
787
|
if (entity.model.columns.length > 0) {
|
|
435
788
|
parts.push(renderColumnTable(entity));
|
|
@@ -437,6 +790,10 @@ function renderEntitySection(entity) {
|
|
|
437
790
|
if (entity.model.constraints.length > 0) {
|
|
438
791
|
parts.push(renderConstraints(entity.model.constraints));
|
|
439
792
|
}
|
|
793
|
+
const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
|
|
794
|
+
if (computedColumns.length > 0) {
|
|
795
|
+
parts.push(renderComputedColumns(computedColumns));
|
|
796
|
+
}
|
|
440
797
|
return parts.join("\n\n");
|
|
441
798
|
}
|
|
442
799
|
function renderColumnTable(entity) {
|
|
@@ -445,59 +802,256 @@ function renderColumnTable(entity) {
|
|
|
445
802
|
const rows = entity.model.columns.map((col) => {
|
|
446
803
|
const key = resolveColumnKey(col);
|
|
447
804
|
const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
|
|
448
|
-
const
|
|
449
|
-
|
|
805
|
+
const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
|
|
806
|
+
const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
|
|
807
|
+
const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
|
|
808
|
+
return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(col.type)} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
|
|
450
809
|
});
|
|
451
810
|
return [header, sep, ...rows].join("\n");
|
|
452
811
|
}
|
|
453
812
|
function resolveColumnKey(col) {
|
|
454
|
-
|
|
813
|
+
const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
|
|
814
|
+
if (col.isPrimary && col.isForeignKey) {
|
|
815
|
+
return `PK, ${fkKey}`;
|
|
816
|
+
}
|
|
817
|
+
if (col.isPrimary) {
|
|
818
|
+
return "PK";
|
|
819
|
+
}
|
|
455
820
|
if (col.isForeignKey) {
|
|
456
|
-
return
|
|
821
|
+
return fkKey;
|
|
822
|
+
}
|
|
823
|
+
if (col.isUnique) {
|
|
824
|
+
return "UK";
|
|
825
|
+
}
|
|
826
|
+
if (col.isDiscriminator) {
|
|
827
|
+
return "discriminator";
|
|
828
|
+
}
|
|
829
|
+
if (col.embeddedIn !== void 0) {
|
|
830
|
+
return `[${col.embeddedIn}]`;
|
|
457
831
|
}
|
|
458
|
-
if (col.isUnique) return "UK";
|
|
459
|
-
if (col.formula !== void 0) return `formula: ${col.formula}`;
|
|
460
|
-
if (col.isDiscriminator) return "discriminator";
|
|
461
|
-
if (col.embeddedIn !== void 0) return `[${col.embeddedIn}]`;
|
|
462
832
|
return "";
|
|
463
833
|
}
|
|
834
|
+
function renderComputedColumns(columns) {
|
|
835
|
+
return renderBulletSection("**Computed columns:**", columns, (col) => {
|
|
836
|
+
const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
|
|
837
|
+
return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
464
840
|
function renderConstraints(constraints) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
841
|
+
return renderBulletSection("**Constraints:**", constraints, (c) => {
|
|
842
|
+
const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
|
|
843
|
+
const properties = c.properties.map(escapeMarkdownInline).join(", ");
|
|
468
844
|
if (c.type === "index") {
|
|
469
|
-
|
|
470
|
-
} else if (c.type === "unique") {
|
|
471
|
-
lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
|
|
472
|
-
} else if (c.type === "check") {
|
|
473
|
-
lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
|
|
845
|
+
return `- Index${name}: (${properties})`;
|
|
474
846
|
}
|
|
475
|
-
|
|
476
|
-
|
|
847
|
+
if (c.type === "unique") {
|
|
848
|
+
return `- Unique${name}: (${properties})`;
|
|
849
|
+
}
|
|
850
|
+
return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
|
|
851
|
+
});
|
|
477
852
|
}
|
|
478
853
|
|
|
479
854
|
// src/index.ts
|
|
855
|
+
var COMPILED_JS = /\.(c|m)?js$/i;
|
|
856
|
+
function resolveJsDocSources(sourcePaths, src, onWarn) {
|
|
857
|
+
if (src !== void 0 && src.length > 0) {
|
|
858
|
+
return src;
|
|
859
|
+
}
|
|
860
|
+
if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
|
|
861
|
+
onWarn(
|
|
862
|
+
'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.'
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return sourcePaths;
|
|
866
|
+
}
|
|
867
|
+
function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
|
|
868
|
+
if (jsDocResult.sourceFileCount === 0) {
|
|
869
|
+
throw new Error(
|
|
870
|
+
`No source files matched the explicit src paths: ${src.join(", ")}
|
|
871
|
+
Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
|
|
875
|
+
const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
|
|
876
|
+
if (missingConcrete.length > 0) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
|
|
879
|
+
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.`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
if (onWarn) {
|
|
883
|
+
const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
|
|
884
|
+
if (missingAbstract.length > 0) {
|
|
885
|
+
onWarn(
|
|
886
|
+
`Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
|
|
887
|
+
@hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function errorMessages(err) {
|
|
893
|
+
const messages = [];
|
|
894
|
+
const seen = /* @__PURE__ */ new Set();
|
|
895
|
+
let current = err;
|
|
896
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
897
|
+
seen.add(current);
|
|
898
|
+
messages.push(current.message);
|
|
899
|
+
current = current.cause;
|
|
900
|
+
}
|
|
901
|
+
return messages;
|
|
902
|
+
}
|
|
903
|
+
function isMissingTsMorphSourceFile(err) {
|
|
904
|
+
return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
|
|
905
|
+
}
|
|
906
|
+
async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
|
|
907
|
+
try {
|
|
908
|
+
return await loadEntityMetadata(effectiveOrm);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
|
|
911
|
+
if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
|
|
912
|
+
throw err;
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
return await loadEntityMetadata(originalOrm);
|
|
916
|
+
} catch {
|
|
917
|
+
throw err;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
480
921
|
async function generateMarkdown(options) {
|
|
481
|
-
const { orm, title = "Database Schema", description, src
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
const
|
|
922
|
+
const { orm, title = "Database Schema", description, src, onWarn } = options;
|
|
923
|
+
const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
|
|
924
|
+
const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
|
|
925
|
+
const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
|
|
926
|
+
if (src !== void 0 && src.length > 0) {
|
|
927
|
+
assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
|
|
928
|
+
}
|
|
929
|
+
const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
|
|
485
930
|
return renderMarkdown(docModel);
|
|
486
931
|
}
|
|
487
932
|
|
|
488
933
|
// src/cli.ts
|
|
934
|
+
function toConfigImportSpecifier(configPath) {
|
|
935
|
+
return (0, import_node_url.pathToFileURL)(path2.resolve(configPath)).href;
|
|
936
|
+
}
|
|
937
|
+
function findNearestTsconfig(fromPath) {
|
|
938
|
+
let dir = path2.dirname(path2.resolve(fromPath));
|
|
939
|
+
for (; ; ) {
|
|
940
|
+
const candidate = path2.join(dir, "tsconfig.json");
|
|
941
|
+
if (syncFs.existsSync(candidate)) {
|
|
942
|
+
return candidate;
|
|
943
|
+
}
|
|
944
|
+
const parent = path2.dirname(dir);
|
|
945
|
+
if (parent === dir) {
|
|
946
|
+
return void 0;
|
|
947
|
+
}
|
|
948
|
+
dir = parent;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async function loadOrmOptions(configPath, tsconfigPath) {
|
|
952
|
+
const isTypeScriptConfig = configPath.endsWith(".ts");
|
|
953
|
+
if (isTypeScriptConfig) {
|
|
954
|
+
let register;
|
|
955
|
+
try {
|
|
956
|
+
({ register } = await import("tsx/esm/api"));
|
|
957
|
+
} catch {
|
|
958
|
+
throw new Error('TypeScript config files require the "tsx" package.\nInstall it with: npm install -D tsx');
|
|
959
|
+
}
|
|
960
|
+
let tsconfig;
|
|
961
|
+
if (tsconfigPath !== void 0) {
|
|
962
|
+
tsconfig = path2.resolve(tsconfigPath);
|
|
963
|
+
if (!syncFs.existsSync(tsconfig)) {
|
|
964
|
+
throw new Error(`--tsconfig file not found: ${tsconfig}`);
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
tsconfig = findNearestTsconfig(configPath);
|
|
968
|
+
}
|
|
969
|
+
register(tsconfig !== void 0 ? { tsconfig } : {});
|
|
970
|
+
}
|
|
971
|
+
const configUrl = toConfigImportSpecifier(configPath);
|
|
972
|
+
let mod;
|
|
973
|
+
try {
|
|
974
|
+
mod = await import(
|
|
975
|
+
/* @vite-ignore */
|
|
976
|
+
configUrl
|
|
977
|
+
);
|
|
978
|
+
} catch (cause) {
|
|
979
|
+
if (isTypeScriptConfig) {
|
|
980
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
981
|
+
throw new Error(
|
|
982
|
+
`Failed to load TypeScript config.
|
|
983
|
+
${detail}
|
|
984
|
+
|
|
985
|
+
If this looks like a decorator/metadata error, the tsconfig applied to your entity files is likely missing "experimentalDecorators" / "emitDecoratorMetadata".
|
|
986
|
+
Make sure a tsconfig.json with those options sits next to your config file, or pass one explicitly with --tsconfig <path>.`,
|
|
987
|
+
{ cause }
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
throw cause;
|
|
991
|
+
}
|
|
992
|
+
if (mod.default === void 0) {
|
|
993
|
+
throw new Error("Config file must use a default export, e.g. `export default defineConfig({ ... })`.");
|
|
994
|
+
}
|
|
995
|
+
const config = mod.default;
|
|
996
|
+
if (typeof config === "function" || config instanceof Promise) {
|
|
997
|
+
throw new Error(
|
|
998
|
+
"Config file default export must be a configuration object, not a function or Promise.\nResolve it first, or use the programmatic API instead (see README)."
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
"Config file default export must be a configuration object, not a primitive value or array.\nExport a plain MikroORM options object instead."
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const options = config;
|
|
1007
|
+
const withPreferTs = isTypeScriptConfig && options.preferTs === void 0 ? { ...options, preferTs: true } : options;
|
|
1008
|
+
return withPreferTs;
|
|
1009
|
+
}
|
|
1010
|
+
function formatErrorChain(err) {
|
|
1011
|
+
const lines = [];
|
|
1012
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1013
|
+
let current = err;
|
|
1014
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
1015
|
+
seen.add(current);
|
|
1016
|
+
lines.push(current.message);
|
|
1017
|
+
current = current.cause;
|
|
1018
|
+
}
|
|
1019
|
+
if (current !== void 0 && !(current instanceof Error)) {
|
|
1020
|
+
lines.push(String(current));
|
|
1021
|
+
}
|
|
1022
|
+
return lines.map((line, i) => i === 0 ? line : ` \u21B3 caused by: ${line}`).join("\n");
|
|
1023
|
+
}
|
|
1024
|
+
var REFLECTION_METADATA_HINT = "\n\nNote: this tool loads configs via tsx (esbuild), which does not emit 'emitDecoratorMetadata' reflection data, so enabling it will not help here.\nEither give each entity property an explicit 'type:'/'entity:' attribute, or install '@mikro-orm/reflection' \u2014 this tool then reads types from your TypeScript sources automatically.";
|
|
1025
|
+
function formatDiscoveryError(err) {
|
|
1026
|
+
const chain = formatErrorChain(err);
|
|
1027
|
+
return chain.includes("emitDecoratorMetadata") ? chain + REFLECTION_METADATA_HINT : chain;
|
|
1028
|
+
}
|
|
1029
|
+
function formatFileSystemError(cause) {
|
|
1030
|
+
if (cause instanceof Error) {
|
|
1031
|
+
const code = "code" in cause && typeof cause.code === "string" ? ` (${cause.code})` : "";
|
|
1032
|
+
return `${cause.message}${code}`;
|
|
1033
|
+
}
|
|
1034
|
+
return String(cause);
|
|
1035
|
+
}
|
|
1036
|
+
async function writeMarkdownFile(outPath, markdown) {
|
|
1037
|
+
try {
|
|
1038
|
+
await fs.mkdir(path2.dirname(outPath), { recursive: true });
|
|
1039
|
+
await fs.writeFile(outPath, markdown, "utf-8");
|
|
1040
|
+
} catch (cause) {
|
|
1041
|
+
throw new Error(`Cannot write output file: ${outPath}
|
|
1042
|
+
${formatFileSystemError(cause)}`, { cause });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
489
1045
|
async function run(opts) {
|
|
490
|
-
const configPath =
|
|
491
|
-
const outPath =
|
|
1046
|
+
const configPath = path2.resolve(opts.config);
|
|
1047
|
+
const outPath = path2.resolve(opts.out);
|
|
492
1048
|
let ormOptions;
|
|
493
1049
|
try {
|
|
494
|
-
|
|
495
|
-
ormOptions = mod.default ?? mod;
|
|
1050
|
+
ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
|
|
496
1051
|
} catch (err) {
|
|
497
|
-
const hint = configPath.endsWith(".ts") ? "\nHint: TypeScript configs require tsx or ts-node:\n npx tsx ./node_modules/.bin/mikro-orm-markdown ..." : "";
|
|
498
1052
|
process.stderr.write(
|
|
499
1053
|
`Error: Cannot load config: ${configPath}
|
|
500
|
-
${err instanceof Error ? err.message : String(err)}
|
|
1054
|
+
${err instanceof Error ? err.message : String(err)}
|
|
501
1055
|
`
|
|
502
1056
|
);
|
|
503
1057
|
process.exit(1);
|
|
@@ -507,28 +1061,58 @@ ${err instanceof Error ? err.message : String(err)}${hint}
|
|
|
507
1061
|
markdown = await generateMarkdown({
|
|
508
1062
|
orm: ormOptions,
|
|
509
1063
|
title: opts.title,
|
|
510
|
-
|
|
511
|
-
...opts.
|
|
1064
|
+
...opts.description !== void 0 && { description: opts.description },
|
|
1065
|
+
...opts.src !== void 0 && { src: opts.src },
|
|
1066
|
+
onWarn: (message) => void process.stderr.write(`Warning: ${message}
|
|
1067
|
+
`)
|
|
512
1068
|
});
|
|
513
1069
|
} catch (err) {
|
|
514
|
-
|
|
515
|
-
process.stderr.write(`Error: ${msg}
|
|
1070
|
+
process.stderr.write(`Error: ${formatDiscoveryError(err)}
|
|
516
1071
|
`);
|
|
517
1072
|
process.exit(1);
|
|
518
1073
|
}
|
|
519
|
-
|
|
520
|
-
|
|
1074
|
+
try {
|
|
1075
|
+
await writeMarkdownFile(outPath, markdown);
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1078
|
+
`);
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
process.stdout.write(`\u2713 Written to ${path2.relative(process.cwd(), outPath)}
|
|
521
1082
|
`);
|
|
522
1083
|
}
|
|
523
1084
|
var program = new import_commander.Command().name("mikro-orm-markdown").description("Generate Mermaid ERD + markdown docs from MikroORM entities").requiredOption("-c, --config <path>", "MikroORM config file path").option("-o, --out <path>", "Output markdown file path", "./ERD.md").option("-t, --title <string>", "Document title", "Database Schema").option("-d, --description <string>", "Optional description paragraph shown below the title").option(
|
|
524
|
-
"
|
|
525
|
-
"
|
|
526
|
-
|
|
527
|
-
|
|
1085
|
+
"--tsconfig <path>",
|
|
1086
|
+
"tsconfig.json to use when loading a .ts config (defaults to the nearest one beside the config file)"
|
|
1087
|
+
).option(
|
|
1088
|
+
"--src <paths...>",
|
|
1089
|
+
"Source .ts file globs to read JSDoc from when entities run from compiled .js (comments are stripped at build time)"
|
|
528
1090
|
).action(run);
|
|
529
|
-
|
|
530
|
-
|
|
1091
|
+
function isDirectCliExecution() {
|
|
1092
|
+
const entryPoint = process.argv[1];
|
|
1093
|
+
if (entryPoint === void 0) {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
return syncFs.realpathSync(path2.resolve(entryPoint)) === syncFs.realpathSync((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
1098
|
+
} catch {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (isDirectCliExecution()) {
|
|
1103
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1104
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
531
1105
|
`);
|
|
532
|
-
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1110
|
+
0 && (module.exports = {
|
|
1111
|
+
findNearestTsconfig,
|
|
1112
|
+
formatDiscoveryError,
|
|
1113
|
+
formatErrorChain,
|
|
1114
|
+
loadOrmOptions,
|
|
1115
|
+
toConfigImportSpecifier,
|
|
1116
|
+
writeMarkdownFile
|
|
533
1117
|
});
|
|
534
1118
|
//# sourceMappingURL=cli.cjs.map
|